v0.33.7
This commit is contained in:
1 parent
162e986d37
commit
6b937aaf85
54 files changed
+3527
-1325
No files matched your search
@@ -0,0 +1,187 @@
|
||||
/// Account-scoped snapshots of Overview order, visibility, widths and display.
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DashboardPresetLayout {
|
||||
dashboard_widgets: Vec<String>,
|
||||
dashboard_hidden_widgets: Vec<String>,
|
||||
dashboard_widget_sizes: Option<BTreeMap<String, u8>>,
|
||||
dashboard_density: String,
|
||||
overview_samples: u16,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct NamedDashboardPreset {
|
||||
name: String,
|
||||
#[serde(flatten)]
|
||||
layout: DashboardPresetLayout,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DashboardPresetRename {
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn valid_dashboard_preset_name(value: &str) -> bool {
|
||||
!value.is_empty()
|
||||
&& value.len() <= 64
|
||||
&& value.trim() == value
|
||||
&& !value.chars().any(|ch| ch.is_control() || ch == '/')
|
||||
}
|
||||
|
||||
fn validate_dashboard_preset_layout(layout: &DashboardPresetLayout) -> Result<(), ApiError> {
|
||||
if !matches!(layout.overview_samples, 60 | 120 | 300 | 600) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid Overview history samples".into(),
|
||||
));
|
||||
}
|
||||
let mut pref = AdminPreferences::default();
|
||||
pref.dashboard_widgets.clone_from(&layout.dashboard_widgets);
|
||||
pref.dashboard_hidden_widgets
|
||||
.clone_from(&layout.dashboard_hidden_widgets);
|
||||
pref.dashboard_widget_sizes
|
||||
.clone_from(&layout.dashboard_widget_sizes);
|
||||
pref.dashboard_density.clone_from(&layout.dashboard_density);
|
||||
validate_admin_preferences(&pref)
|
||||
}
|
||||
|
||||
async fn dashboard_presets(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
) -> Result<Json<Vec<NamedDashboardPreset>>, ApiError> {
|
||||
let entries = state
|
||||
.store
|
||||
.list_admin_dashboard_presets(&session.username)
|
||||
.await
|
||||
.map_err(auth::internal)?;
|
||||
let mut presets = Vec::with_capacity(entries.len());
|
||||
for (name, document) in entries {
|
||||
let Ok(mut layout) = serde_json::from_str::<DashboardPresetLayout>(&document) else {
|
||||
tracing::warn!("skipped invalid saved Overview preset");
|
||||
continue;
|
||||
};
|
||||
// Compatibility with future/retired widgets: use the same registry
|
||||
// reconciliation logic as ordinary saved administrator preferences.
|
||||
let mut prefs = AdminPreferences::default();
|
||||
prefs.dashboard_widgets = layout.dashboard_widgets;
|
||||
prefs.dashboard_hidden_widgets = layout.dashboard_hidden_widgets;
|
||||
prefs.dashboard_widget_sizes = layout.dashboard_widget_sizes;
|
||||
reconcile_dashboard_widgets(&mut prefs);
|
||||
layout.dashboard_widgets = prefs.dashboard_widgets;
|
||||
layout.dashboard_hidden_widgets = prefs.dashboard_hidden_widgets;
|
||||
layout.dashboard_widget_sizes = prefs.dashboard_widget_sizes;
|
||||
if validate_dashboard_preset_layout(&layout).is_ok() {
|
||||
presets.push(NamedDashboardPreset { name, layout });
|
||||
}
|
||||
}
|
||||
Ok(Json(presets))
|
||||
}
|
||||
|
||||
async fn save_dashboard_preset(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Path(name): Path<String>,
|
||||
Json(layout): Json<DashboardPresetLayout>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
if !valid_dashboard_preset_name(&name) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid dashboard preset name".into(),
|
||||
));
|
||||
}
|
||||
validate_dashboard_preset_layout(&layout)?;
|
||||
let document = serde_json::to_string(&layout).map_err(auth::internal)?;
|
||||
if !state
|
||||
.store
|
||||
.save_admin_dashboard_preset(&session.username, &name, &document)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
{
|
||||
return Err(ApiError(
|
||||
StatusCode::CONFLICT,
|
||||
"dashboard preset limit (16) reached".into(),
|
||||
));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn delete_dashboard_preset(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
if !state
|
||||
.store
|
||||
.delete_admin_dashboard_preset(&session.username, &name)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
{
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"dashboard preset not found".into(),
|
||||
));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn rename_dashboard_preset(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Path(name): Path<String>,
|
||||
Json(rename): Json<DashboardPresetRename>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
if !valid_dashboard_preset_name(&rename.name) || !valid_dashboard_preset_name(&name) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid dashboard preset name".into(),
|
||||
));
|
||||
}
|
||||
if !state
|
||||
.store
|
||||
.rename_admin_dashboard_preset(&session.username, &name, &rename.name)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
{
|
||||
return Err(ApiError(
|
||||
StatusCode::CONFLICT,
|
||||
"dashboard preset missing or name in use".into(),
|
||||
));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dashboard_preset_validation_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn names_are_bounded_and_paths_are_not_accepted() {
|
||||
assert!(valid_dashboard_preset_name("Work layout"));
|
||||
assert!(valid_dashboard_preset_name("Układ na telefon"));
|
||||
for invalid in ["", " name", "name ", "bad/name", "bad\nname"] {
|
||||
assert!(!valid_dashboard_preset_name(invalid));
|
||||
}
|
||||
assert!(!valid_dashboard_preset_name(&"X".repeat(65)));
|
||||
}
|
||||
#[test]
|
||||
fn layouts_reject_invalid_history_and_widgets() {
|
||||
let mut layout = DashboardPresetLayout {
|
||||
dashboard_widgets: default_admin_dashboard_widgets_for_tests(),
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_density: default_admin_dashboard_density(),
|
||||
overview_samples: 600,
|
||||
};
|
||||
assert!(validate_dashboard_preset_layout(&layout).is_ok());
|
||||
layout.overview_samples = 601;
|
||||
assert!(validate_dashboard_preset_layout(&layout).is_err());
|
||||
layout.overview_samples = 600;
|
||||
layout.dashboard_widgets.push("undefined-widget".into());
|
||||
assert!(validate_dashboard_preset_layout(&layout).is_err());
|
||||
}
|
||||
fn default_admin_dashboard_widgets_for_tests() -> Vec<String> {
|
||||
AdminPreferences::default().dashboard_widgets
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ include!("router.rs");
|
||||
include!("access.rs");
|
||||
include!("session.rs");
|
||||
include!("preferences.rs");
|
||||
include!("dashboard_presets.rs");
|
||||
include!("status.rs");
|
||||
include!("logging_api.rs");
|
||||
include!("file_logs.rs");
|
||||
|
||||
+389
-9
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.33.4",
|
||||
"version": "0.33.7",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
@@ -1241,15 +1241,37 @@
|
||||
"type": "object",
|
||||
"description": "Cached OS/process metrics for fast SSE/live charts only; full System details remain on /api/system-info",
|
||||
"properties": {
|
||||
"sampled_at_ms": { "type": "integer", "format": "int64" },
|
||||
"cpu_percent": { "type": "number", "nullable": true },
|
||||
"process_cpu_percent": { "type": "number", "nullable": true },
|
||||
"process_rss_bytes": { "type": "integer", "format": "int64", "nullable": true },
|
||||
"network_rx_bytes_per_second": { "type": "number", "nullable": true },
|
||||
"network_tx_bytes_per_second": { "type": "number", "nullable": true },
|
||||
"sampled_at_ms": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"cpu_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"process_cpu_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"process_rss_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true
|
||||
},
|
||||
"network_rx_bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"network_tx_bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"network_interfaces": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "additionalProperties": true }
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -3570,7 +3592,13 @@
|
||||
"maxProperties": 78,
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"enum": [2, 3, 4, 6, 12]
|
||||
"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."
|
||||
},
|
||||
@@ -6631,6 +6659,82 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DashboardPresetLayout": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Validated Overview snapshot. Factory layout is separate from stored user presets.",
|
||||
"properties": {
|
||||
"dashboard_widgets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"description": "Ordered registry widget identifiers"
|
||||
},
|
||||
"dashboard_hidden_widgets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Invisible registry widget IDs"
|
||||
},
|
||||
"dashboard_widget_sizes": {
|
||||
"nullable": true,
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"minimum": 2,
|
||||
"maximum": 12
|
||||
},
|
||||
"description": "Width overrides in a 12-column grid; null selects legacy groups"
|
||||
},
|
||||
"dashboard_density": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"normal",
|
||||
"compact",
|
||||
"ultra"
|
||||
]
|
||||
},
|
||||
"overview_samples": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
60,
|
||||
120,
|
||||
300,
|
||||
600
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dashboard_widgets",
|
||||
"dashboard_hidden_widgets",
|
||||
"dashboard_widget_sizes",
|
||||
"dashboard_density",
|
||||
"overview_samples"
|
||||
]
|
||||
},
|
||||
"NamedDashboardPreset": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DashboardPresetLayout"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -11931,6 +12035,282 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/dashboard-presets": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "List administrator dashboard presets",
|
||||
"operationId": "get_dashboard_presets",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Named layouts owned by the authenticated administrator",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NamedDashboardPreset"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/dashboard-presets/{name}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "URL-encoded user-defined preset name (1-64 UTF-8 bytes)",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"put": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "Save or overwrite an Overview dashboard preset",
|
||||
"operationId": "put_dashboard_preset",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DashboardPresetLayout"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Preset saved"
|
||||
},
|
||||
"409": {
|
||||
"description": "Account already has 16 named presets",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid preset name or layout",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "Rename an Overview dashboard preset",
|
||||
"operationId": "patch_dashboard_preset",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Preset renamed"
|
||||
},
|
||||
"409": {
|
||||
"description": "Source name not found or target name already exists",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid preset name",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "Delete an Overview dashboard preset",
|
||||
"operationId": "delete_dashboard_preset",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Preset deleted"
|
||||
},
|
||||
"404": {
|
||||
"description": "Preset not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-3
@@ -1,6 +1,14 @@
|
||||
//! Deliberately small anonymous surface. Operational templates/assets are private.
|
||||
use crate::{auth::Session, runtime::{ApiError, App}, settings::PanelCompression};
|
||||
use axum::{extract::{rejection::JsonRejection, State}, response::{Html, IntoResponse, Response}, Extension};
|
||||
use crate::{
|
||||
auth::Session,
|
||||
runtime::{ApiError, App},
|
||||
settings::PanelCompression,
|
||||
};
|
||||
use axum::{
|
||||
extract::{rejection::JsonRejection, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
Extension,
|
||||
};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
@@ -10,7 +18,19 @@ static ADMIN_INDEX: OnceLock<PanelContent> = OnceLock::new();
|
||||
|
||||
fn panel_response(headers: &HeaderMap, compression: &PanelCompression) -> Response {
|
||||
let content = ADMIN_INDEX.get_or_init(|| {
|
||||
// Compile-time embedded HTML fragments are assembled once at startup.
|
||||
// No additional browser requests, changed DOM nodes or script ordering.
|
||||
let (metrics, web_metrics) = include_str!("web/partials/overview-metrics.html")
|
||||
.split_once("<!-- RP_METRICS_SPLIT -->")
|
||||
.expect("invalid Overview metrics partial");
|
||||
let (charts, web_charts) = include_str!("web/partials/overview-charts.html")
|
||||
.split_once("<!-- RP_CHARTS_SPLIT -->")
|
||||
.expect("invalid Overview charts partial");
|
||||
let html = include_str!("web/index.html")
|
||||
.replace("<!-- RP_METRICS_MAIN -->", metrics)
|
||||
.replace("<!-- RP_CHARTS_MAIN -->", charts)
|
||||
.replace("<!-- RP_METRICS_WEB -->", web_metrics)
|
||||
.replace("<!-- RP_CHARTS_WEB -->", web_charts)
|
||||
.replace("__RUSTPROXY_ASSET_ID__", super::asset_build_id())
|
||||
.replace("__RUSTPROXY_VERSION__", env!("CARGO_PKG_VERSION"));
|
||||
// Exactly one static panel document shared between all administrator accounts.
|
||||
@@ -30,7 +50,10 @@ pub(super) async fn index(
|
||||
return Html(html).into_response();
|
||||
}
|
||||
let snapshot = state.config.load();
|
||||
panel_response(&headers, &snapshot.envelope.config.administration.panel_compression)
|
||||
panel_response(
|
||||
&headers,
|
||||
&snapshot.envelope.config.administration.panel_compression,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn invalid_json(error: JsonRejection) -> ApiError {
|
||||
|
||||
+9
-1
@@ -69,7 +69,7 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/dashboard-widgets.json"),
|
||||
");",
|
||||
include_str!("web/app/preferences.js"),
|
||||
include_str!("web/app/dashboard-layout-editor.js"),
|
||||
include_str!("web/app/dashboard-presets.js"),
|
||||
include_str!("web/app/dashboard-inline-layout.js"),
|
||||
include_str!("web/app/stream-dashboard.js"),
|
||||
include_str!("web/app/backend-dashboard.js"),
|
||||
@@ -249,6 +249,14 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
.route("/api/file-logs", get(file_logs))
|
||||
.route("/api/file-logs/query", get(file_logs_query))
|
||||
.route("/api/preferences", get(preferences).put(update_preferences))
|
||||
.route("/api/dashboard-presets", get(dashboard_presets))
|
||||
.route(
|
||||
"/api/dashboard-presets/{name}",
|
||||
put(save_dashboard_preset)
|
||||
.patch(rename_dashboard_preset)
|
||||
.delete(delete_dashboard_preset)
|
||||
.layer(DefaultBodyLimit::max(10240)),
|
||||
)
|
||||
.route("/api/audit", get(audit))
|
||||
.route("/api/events", get(events))
|
||||
.route("/metrics", get(metrics))
|
||||
|
||||
@@ -1639,6 +1639,87 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn named_dashboard_presets_are_private_validated_and_persistent() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
let preset = DashboardPresetLayout {
|
||||
dashboard_widgets: AdminPreferences::default().dashboard_widgets,
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_density: "compact".to_owned(),
|
||||
overview_samples: 600,
|
||||
};
|
||||
let body = serde_json::to_string(&preset).unwrap();
|
||||
let anon = router.clone().oneshot(
|
||||
request("/api/dashboard-presets").body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(anon.status(), StatusCode::UNAUTHORIZED);
|
||||
let forbidden = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Daily").method("PUT")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.clone())).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(forbidden.status(), StatusCode::FORBIDDEN);
|
||||
let saved = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Daily").method("PUT")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.clone())).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(saved.status(), StatusCode::NO_CONTENT);
|
||||
let listing = router.clone().oneshot(
|
||||
request("/api/dashboard-presets")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(listing.status(), StatusCode::OK);
|
||||
let entries = json_body(listing).await;
|
||||
assert_eq!(entries[0]["name"], "Daily");
|
||||
assert_eq!(entries[0]["overview_samples"], 600);
|
||||
assert_eq!(entries[0]["dashboard_widget_sizes"], Value::Null);
|
||||
assert_eq!(store.list_admin_dashboard_presets("owner").await.unwrap().len(), 1);
|
||||
assert!(store.list_admin_dashboard_presets("someone-else").await.unwrap().is_empty());
|
||||
let rejected = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Invalid").method("PUT")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.replace("\"overview_samples\":600", "\"overview_samples\":999"))).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(rejected.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
let renamed = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Daily").method("PATCH")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(r#"{"name":"Nightly"}"#)).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(renamed.status(), StatusCode::NO_CONTENT);
|
||||
assert_eq!(store.list_admin_dashboard_presets("owner").await.unwrap()[0].0, "Nightly");
|
||||
let deleted = router.oneshot(
|
||||
request("/api/dashboard-presets/Nightly").method("DELETE")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(deleted.status(), StatusCode::NO_CONTENT);
|
||||
assert!(store.list_admin_dashboard_presets("owner").await.unwrap().is_empty());
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_preferences_are_account_scoped_and_csrf_guarded() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
@@ -3010,6 +3091,7 @@ mod tests {
|
||||
"/api/system-info",
|
||||
"/api/config",
|
||||
"/api/preferences",
|
||||
"/api/dashboard-presets",
|
||||
"/api/cache",
|
||||
"/api/cache/bans",
|
||||
"/api/audit",
|
||||
@@ -3044,6 +3126,9 @@ mod tests {
|
||||
("DELETE", "/api/cache/bans/1"),
|
||||
("PUT", "/api/config/cache"),
|
||||
("PUT", "/api/preferences"),
|
||||
("PUT", "/api/dashboard-presets/example"),
|
||||
("DELETE", "/api/dashboard-presets/example"),
|
||||
("PATCH", "/api/dashboard-presets/example"),
|
||||
("POST", "/api/auth/logout-all"),
|
||||
] {
|
||||
let no_session = request(path).method(method).body(Body::empty()).unwrap();
|
||||
|
||||
@@ -162,7 +162,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
draft = original = null;
|
||||
}
|
||||
async function begin() {
|
||||
if (draft || opening || saving || $("overview-layout-dialog")?.open) return;
|
||||
if (draft || opening || saving || $("overview-presets-dialog")?.open) return;
|
||||
opening = true;
|
||||
unlock.disabled = true;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
window.rustProxyDashboardLayout = (() => {
|
||||
const dialog = $("overview-presets-dialog");
|
||||
const list = $("overview-presets-list");
|
||||
const input = $("overview-presets-name");
|
||||
const message = $("overview-presets-message");
|
||||
const save = $("overview-presets-save");
|
||||
const factory = $("overview-presets-factory");
|
||||
const close = $("overview-presets-close");
|
||||
let presets = [];
|
||||
let busy = false;
|
||||
|
||||
const presetPath = name => `/api/dashboard-presets/${encodeURIComponent(name)}`;
|
||||
const nameValid = name => name.length > 0 && new TextEncoder().encode(name).length <= 64
|
||||
&& !/[\x00-\x1f\x7f/]/.test(name);
|
||||
const snapshot = () => {
|
||||
const current = window.rustProxyPreferences.current() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
return {
|
||||
dashboard_widgets: [...normalizeDashboardWidgets(current.dashboard_widgets)],
|
||||
dashboard_hidden_widgets: [...normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets)],
|
||||
dashboard_widget_sizes: normalizeDashboardWidgetSizes(current.dashboard_widget_sizes),
|
||||
dashboard_density: normalizeAdminDensity(current.dashboard_density, "compact"),
|
||||
overview_samples: window.rustProxyOverviewSamples.getLimit(),
|
||||
};
|
||||
};
|
||||
const translateName = (key, name) => t(key).replace("{name}", name);
|
||||
function notify(key, isError = false) {
|
||||
message.textContent = isError ? key : t(key);
|
||||
message.classList.toggle("error", isError);
|
||||
}
|
||||
function lock(value) {
|
||||
busy = value;
|
||||
for (const control of [save, factory, close, input, ...list.querySelectorAll("button")])
|
||||
if (control) control.disabled = value;
|
||||
}
|
||||
async function run(fn) {
|
||||
if (busy) return;
|
||||
lock(true);
|
||||
try { await fn(); }
|
||||
catch (error) {
|
||||
const message = error.status === 409
|
||||
? (String(error.code || "").includes("limit (16)")
|
||||
? t("dashboardPresetsLimit") : t("dashboardPresetsConflict"))
|
||||
: (error.message || String(error));
|
||||
notify(message, true);
|
||||
}
|
||||
finally { lock(false); }
|
||||
}
|
||||
async function reload() {
|
||||
presets = await api("/api/dashboard-presets");
|
||||
render();
|
||||
}
|
||||
function entryButton(key, className, fn, presetName) {
|
||||
const button = el("button", t(key), className);
|
||||
button.type = "button";
|
||||
button.setAttribute("aria-label", `${t(key)}: ${presetName}`);
|
||||
button.dataset.presetAction = key;
|
||||
button.addEventListener("click", () => void run(fn));
|
||||
return button;
|
||||
}
|
||||
function render() {
|
||||
const elements = presets.map(preset => {
|
||||
const row = el("div", undefined, "overview-presets-row");
|
||||
row.setAttribute("role", "listitem");
|
||||
const actions = el("div", undefined, "overview-presets-actions");
|
||||
actions.append(
|
||||
entryButton("dashboardPresetsLoad", "primary", async () => {
|
||||
await apply(preset);
|
||||
dialog.close();
|
||||
toast(t("dashboardPresetsLoaded"));
|
||||
}, preset.name),
|
||||
entryButton("dashboardPresetsOverwrite", "outline", async () => {
|
||||
if (!(await appConfirm(translateName("dashboardPresetsOverwriteConfirm", preset.name)))) return;
|
||||
await api(presetPath(preset.name), { method: "PUT", body: JSON.stringify(snapshot()) });
|
||||
await reload(); notify("dashboardPresetsSaved");
|
||||
}, preset.name),
|
||||
entryButton("dashboardPresetsRename", "quiet", async () => {
|
||||
const name = input.value.trim();
|
||||
if (!nameValid(name)) { notify("dashboardPresetsNameRequired"); return; }
|
||||
if (name === preset.name) return;
|
||||
await api(presetPath(preset.name), { method: "PATCH", body: JSON.stringify({ name }) });
|
||||
input.value = "";
|
||||
await reload(); notify("dashboardPresetsRenamed");
|
||||
}, preset.name),
|
||||
entryButton("dashboardPresetsDelete", "quiet", async () => {
|
||||
if (!(await appConfirm(translateName("dashboardPresetsDeleteConfirm", preset.name), { danger: true }))) return;
|
||||
await api(presetPath(preset.name), { method: "DELETE" });
|
||||
await reload(); notify("dashboardPresetsDeleted");
|
||||
}, preset.name),
|
||||
);
|
||||
row.append(el("strong", preset.name), actions);
|
||||
return row;
|
||||
});
|
||||
list.replaceChildren(...(elements.length ? elements : [el("p", t("dashboardPresetsEmpty"), "fine")]));
|
||||
}
|
||||
async function apply(layout) {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...layout.dashboard_widgets],
|
||||
dashboard_hidden_widgets: [...layout.dashboard_hidden_widgets],
|
||||
dashboard_widget_sizes: layout.dashboard_widget_sizes === null ? null : { ...layout.dashboard_widget_sizes },
|
||||
dashboard_density: layout.dashboard_density,
|
||||
});
|
||||
window.rustProxyOverviewSamples.setLimit(layout.overview_samples, true);
|
||||
}
|
||||
async function open() {
|
||||
if (dialog?.open || busy || window.rustProxyInlineLayout?.isEditing?.()) return;
|
||||
await run(async () => {
|
||||
await window.rustProxyPreferences.load();
|
||||
await reload();
|
||||
input.value = "";
|
||||
notify("");
|
||||
dialog.showModal();
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
save?.addEventListener("click", () => void run(async () => {
|
||||
const name = input.value.trim();
|
||||
if (!nameValid(name)) { notify("dashboardPresetsNameRequired"); return; }
|
||||
if (presets.length >= 16 && !presets.some(preset => preset.name === name)) {
|
||||
notify("dashboardPresetsLimit"); return;
|
||||
}
|
||||
if (presets.some(preset => preset.name === name)
|
||||
&& !(await appConfirm(translateName("dashboardPresetsOverwriteConfirm", name)))) return;
|
||||
await api(presetPath(name), { method: "PUT", body: JSON.stringify(snapshot()) });
|
||||
input.value = "";
|
||||
await reload(); notify("dashboardPresetsSaved");
|
||||
}));
|
||||
factory?.addEventListener("click", () => void run(async () => {
|
||||
if (!(await appConfirm(t("dashboardPresetsResetConfirm")))) return;
|
||||
await apply({
|
||||
dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS],
|
||||
dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS],
|
||||
dashboard_widget_sizes: null,
|
||||
dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density,
|
||||
overview_samples: ADMIN_DASHBOARD_DEFAULTS.overview_samples,
|
||||
});
|
||||
dialog.close();
|
||||
toast(t("dashboardPresetsRestored"));
|
||||
}));
|
||||
close?.addEventListener("click", () => { if (!busy) dialog.close(); });
|
||||
dialog?.addEventListener("cancel", event => { if (busy) event.preventDefault(); });
|
||||
for (const id of ["overview-layout-open", "overview-layout-open-settings"])
|
||||
$(id)?.addEventListener("click", () => { void open(); });
|
||||
return { open, refresh: () => { if (dialog?.open) render(); } };
|
||||
})();
|
||||
+25
-1055
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,373 @@
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_protocol_rates" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideProtocolRates">HTTP/1.1 · HTTP/2 · HTTP/3 traffic</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_protocol_rates-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_protocol_rates-h1-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_protocol_rates-h2-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="overview-wide-wide_protocol_rates-h3-line" d="">
|
||||
</path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span>H1</span><b
|
||||
id="overview-wide-wide_protocol_rates-h1-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span>H2</span><b
|
||||
id="overview-wide-wide_protocol_rates-h2-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span>H3</span><b
|
||||
id="overview-wide-wide_protocol_rates-h3-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_cpu" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCpu">System and process CPU</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_cpu-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_cpu-system-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_cpu-process-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideSystem">System</span><b
|
||||
id="overview-wide-wide_cpu-system-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideProcess">Process</span><b
|
||||
id="overview-wide-wide_cpu-process-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_memory" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideMemory">Process RAM and cache memory</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_memory-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_memory-process-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_memory-cache-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideProcess">Process</span><b
|
||||
id="overview-wide-wide_memory-process-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideCache">Cache</span><b
|
||||
id="overview-wide-wide_memory-cache-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_cache_activity" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCacheActivity">Cache hits / misses per second</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_cache_activity-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_cache_activity-hits-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_cache_activity-misses-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideHits">HIT</span><b
|
||||
id="overview-wide-wide_cache_activity-hits-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideMisses">MISS</span><b
|
||||
id="overview-wide-wide_cache_activity-misses-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_connections" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideConnections">Connections and WebSocket tunnels</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_connections-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_connections-open-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_connections-websockets-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideOpen">Connections</span><b
|
||||
id="overview-wide-wide_connections-open-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideWebsockets">WebSockets</span><b
|
||||
id="overview-wide-wide_connections-websockets-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_backend_health" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideBackendHealth">Backend availability over time</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_backend_health-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_backend_health-up-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_backend_health-down-line" d="">
|
||||
</path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideHealthy">Healthy</span><b
|
||||
id="overview-wide-wide_backend_health-up-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideUnhealthy">Unhealthy</span><b
|
||||
id="overview-wide-wide_backend_health-down-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_latency" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideLatency">Response and backend probe latency</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_latency-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_latency-response-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_latency-probe-line" d="">
|
||||
</path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideResponse">HTTP</span><b
|
||||
id="overview-wide-wide_latency-response-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideProbe">Probes</span><b
|
||||
id="overview-wide-wide_latency-probe-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_transform_activity">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideTransformActivity">Transformation activity</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_transform_activity-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
id="overview-wide-wide_transform_activity-applied-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1"
|
||||
id="overview-wide-wide_transform_activity-cache_hits-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2"
|
||||
id="overview-wide-wide_transform_activity-skipped-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3" id="overview-wide-wide_transform_activity-errors-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewTransformApplied">Applied</span><b
|
||||
id="overview-wide-wide_transform_activity-applied-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewTransformCacheHits">Cache HIT</span><b
|
||||
id="overview-wide-wide_transform_activity-cache_hits-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span data-i18n="overviewTransformSkipped">Skipped</span><b
|
||||
id="overview-wide-wide_transform_activity-skipped-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span data-i18n="overviewTransformErrors">Errors</span><b
|
||||
id="overview-wide-wide_transform_activity-errors-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_compression_activity">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCompressionActivity">Compression activity</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_compression_activity-summary">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_compression_activity-gzip-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_compression_activity-br-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="overview-wide-wide_compression_activity-zstd-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3"
|
||||
id="overview-wide-wide_compression_activity-deflate-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span>gzip</span><b
|
||||
id="overview-wide-wide_compression_activity-gzip-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span>Brotli</span><b
|
||||
id="overview-wide-wide_compression_activity-br-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span>Zstd</span><b
|
||||
id="overview-wide-wide_compression_activity-zstd-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span>deflate</span><b
|
||||
id="overview-wide-wide_compression_activity-deflate-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<!-- RP_CHARTS_SPLIT -->
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_requests" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricRequests">webMetricRequests</h2><span class="overview-wide-summary"
|
||||
id="www_requests-value">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_requests-all-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_requests-php-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesAll">all</span><b id="www_requests-all-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_requests-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_latency" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricLatency">webMetricLatency</h2><span class="overview-wide-summary"
|
||||
id="www_latency-value">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_latency-web-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_latency-php-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesWeb">web</span><b id="www_latency-web-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_latency-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_failures" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricFailures">webMetricFailures</h2><span class="overview-wide-summary"
|
||||
id="www_failures-value">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_failures-timeout-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_failures-connection-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="www_failures-protocol-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3" id="www_failures-capacity-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesTimeout">timeout</span><b id="www_failures-timeout-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesConnection">connection</span><b
|
||||
id="www_failures-connection-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span data-i18n="webMetricSeriesProtocol">protocol</span><b
|
||||
id="www_failures-protocol-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span data-i18n="webMetricSeriesCapacity">capacity</span><b
|
||||
id="www_failures-capacity-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_traffic" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricTraffic">webMetricTraffic</h2><span class="overview-wide-summary"
|
||||
id="www_traffic-value">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_traffic-nonphp-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_traffic-php-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesNonphp">nonphp</span><b id="www_traffic-nonphp-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_traffic-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_status_trend" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricStatusTrend">webMetricStatusTrend</h2><span class="overview-wide-summary"
|
||||
id="www_status_trend-value">-</span>
|
||||
</div>
|
||||
<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 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_status_trend-2xx-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_status_trend-3xx-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="www_status_trend-4xx-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3" id="www_status_trend-5xx-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesTwoxx">2xx</span><b id="www_status_trend-2xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesThreexx">3xx</span><b
|
||||
id="www_status_trend-3xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span data-i18n="webMetricSeriesFourxx">4xx</span><b
|
||||
id="www_status_trend-4xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span data-i18n="webMetricSeriesFivexx">5xx</span><b
|
||||
id="www_status_trend-5xx-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_top_services" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricTopServices">Top Web Services</h2>
|
||||
<span class="overview-wide-summary" id="www_top_services-value">-</span>
|
||||
</div>
|
||||
<div id="www_top_services-bars" class="www-top-bars"></div>
|
||||
<small class="fine" data-i18n="webMetricTopServicesHelp">Requests per second by Web
|
||||
Service.</small>
|
||||
</article>
|
||||
<article class="card activity-card" data-dashboard-widget="traffic_chart">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="requestsPerSecond">Requests per second</h2>
|
||||
<span id="chart-peak">max 0.0 req/s</span>
|
||||
</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>
|
||||
<polyline class="traffic-line traffic-line-h1" id="sparkline-h1" points="0,120 600,120">
|
||||
</polyline>
|
||||
<polyline class="traffic-line traffic-line-h2" id="sparkline-h2" points="0,120 600,120">
|
||||
</polyline>
|
||||
<polyline class="traffic-line traffic-line-h3" id="sparkline-h3" points="0,120 600,120">
|
||||
</polyline>
|
||||
<polyline class="traffic-line traffic-line-transform" id="sparkline-transform" points="0,120 600,120">
|
||||
</polyline>
|
||||
</svg>
|
||||
<div class="traffic-protocol-legend" aria-label="Request rate by HTTP protocol and transformed response rate">
|
||||
<span class="legend-total"><i></i>Total <b id="rate-total">0.0/s</b></span>
|
||||
<span class="legend-h1"><i></i>HTTP/1.1 <b id="rate-h1">0.0/s</b></span>
|
||||
<span class="legend-h2"><i></i>HTTP/2 <b id="rate-h2">0.0/s</b></span>
|
||||
<span class="legend-h3"><i></i>HTTP/3 <b id="rate-h3">0.0/s</b></span>
|
||||
<span class="legend-transform"><i></i><span data-i18n="liveTransformed">Transformed</span> <b
|
||||
id="rate-transform">0.0/s</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card system-card" data-dashboard-widget="system_chart">
|
||||
<div class="card-heading">
|
||||
<div>
|
||||
<h3 class="overview-chart-heading" data-i18n="systemLoad">System load</h3>
|
||||
<h2><span id="system-cpu">-</span> <small>CPU</small></h2>
|
||||
</div><span class="meta" id="system-source">-</span>
|
||||
</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>
|
||||
<div class="memory-line"><span data-i18n="systemMemory">System RAM</span><b id="system-memory">-</b></div>
|
||||
<div class="overview-meter system-memory-meter" id="memory-meter" role="progressbar" aria-label="System RAM"
|
||||
data-i18n-aria="systemMemory" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="memory-meter-fill"></span>
|
||||
</div>
|
||||
<div class="memory-line"><span data-i18n="systemSwap">System swap</span><b id="system-swap">-</b></div>
|
||||
<div class="chart-footer"><span data-i18n="loadAverage">Load 1 / 5 / 15 min</span><span id="system-load">-</span>
|
||||
</div>
|
||||
<div class="chart-footer"><span data-i18n="systemUptime">System uptime</span><span id="system-uptime">-</span></div>
|
||||
</article>
|
||||
<article class="card connection-card" data-dashboard-widget="connection_chart">
|
||||
<div class="card-heading">
|
||||
<div>
|
||||
<h3 class="overview-chart-heading" data-i18n="connectionActivity">Connection activity</h3>
|
||||
<h2><span id="connection-current">0</span> <small data-i18n="connections">Open
|
||||
connections</small></h2>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<polyline class="connection-exchange-line" id="connection-exchange-sparkline" points="0,95 300,95"></polyline>
|
||||
</svg>
|
||||
<div class="connection-legend"><span class="connection-open-key"><i></i><span data-i18n="connections">Open
|
||||
connections</span> <b id="connection-open-value">0</b></span><span
|
||||
class="connection-exchange-key"><i></i><span data-i18n="activeExchanges">Active
|
||||
exchanges</span> <b id="connection-exchange-value">0</b></span></div>
|
||||
<div class="chart-footer"><span><span data-i18n="backends">Backends</span>: <b id="connection-backends">0</b></span>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,471 @@
|
||||
<article class="stat" data-dashboard-widget="requests"><span data-i18n="requests">Requests</span><strong
|
||||
id="metric-requests">0</strong><small id="request-rate">0 req/s</small></article>
|
||||
<article class="stat" data-dashboard-widget="active"><span data-i18n="activeExchanges">Active
|
||||
exchanges</span><strong id="metric-active">0</strong><small data-i18n="bodyLifetime">Full body lifetime</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="bandwidth"><span data-i18n="bandwidth">Payload
|
||||
transferred</span><strong id="metric-bytes">0 B</strong><small class="payload-split"
|
||||
id="metric-bytes-split"><span>↓ <b id="metric-bytes-down">0 B</b></span><span>↑ <b id="metric-bytes-up">0
|
||||
B</b></span></small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="errors"><span data-i18n="errors">Errors</span><strong
|
||||
id="metric-errors">0</strong><small data-i18n="errorDescription">5xx / body /
|
||||
cancelled</small></article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="cache"><span data-i18n="cacheHitRatio">Cache hit
|
||||
ratio</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="cache-ratio-chart" role="img" aria-label="Cache HIT and MISS distribution"
|
||||
viewBox="0 0 64 64">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="overview-donut-hit" id="cache-ratio-hit-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
<circle class="overview-donut-miss" id="cache-ratio-miss-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
</svg>
|
||||
<div class="overview-donut-info">
|
||||
<strong id="metric-cache-ratio">-</strong>
|
||||
<small class="overview-donut-legend" id="cache-summary">
|
||||
<span class="overview-donut-hit-key"><i></i>HIT <b id="cache-ratio-hit-count">0</b></span>
|
||||
<span class="overview-donut-miss-key"><i></i>MISS <b id="cache-ratio-miss-count">0</b></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="memory"><span data-i18n="processMemory">Process
|
||||
memory</span><strong id="metric-rss">-</strong><small id="process-summary">-</small>
|
||||
</article>
|
||||
<article class="stat stat-with-action" data-dashboard-widget="websocket"><span data-i18n="wsActive">WebSocket
|
||||
tunnels</span><button class="quiet stat-corner-action" data-i18n="tunnelsOpen" id="tunnels-open"
|
||||
type="button">Active tunnels</button><strong id="metric-ws">0</strong><small
|
||||
aria-label="Active WebSocket tunnels by frontend protocol"><span data-i18n="wsH1">H1
|
||||
Upgrade</span> <span id="metric-ws-h1">0</span> · <span data-i18n="wsH2Rfc8441">H2
|
||||
RFC 8441</span> <span id="metric-ws-h2">0</span> · <span data-i18n="wsH3Rfc9220">H3
|
||||
RFC 9220</span> <span id="metric-ws-h3">0</span><br><span data-i18n="wsEstablished">Established</span>:
|
||||
<span id="metric-ws-total">0</span></small></article>
|
||||
<article class="stat" data-dashboard-widget="http2"><span data-i18n="h2Requests">HTTP/2
|
||||
requests</span><strong id="metric-h2">0</strong><small><span data-i18n="h2ActiveConnections">Active H2
|
||||
connections</span>: <span id="metric-h2-active">0</span></small></article>
|
||||
<article class="stat" data-dashboard-widget="http3"><span data-i18n="h3Requests">HTTP/3
|
||||
requests</span><strong id="metric-h3">0</strong></article>
|
||||
<article class="stat" data-dashboard-widget="tls"><span data-i18n="tlsFailures">TLS handshake
|
||||
errors</span><strong id="metric-tls-errors">0</strong></article>
|
||||
<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 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>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="http-protocol-h2" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line proto-h3-line" id="http-protocol-h3" points="0,55 300,55"></polyline>
|
||||
</svg><small class="stat-chart-legend"><span class="legend-h1"><i></i>H1 <b
|
||||
id="metric-http-proto-h1">0.0/s</b></span><span class="legend-h2"><i></i>H2 <b
|
||||
id="metric-http-proto-h2">0.0/s</b></span><span class="legend-h3"><i></i>H3 <b
|
||||
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 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>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="ws-protocol-h1" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="ws-protocol-h2" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line proto-h3-line" id="ws-protocol-h3" points="0,55 300,55"></polyline>
|
||||
</svg><small class="stat-chart-legend"><span class="legend-h1"><i></i>H1 <b
|
||||
id="metric-ws-proto-h1">0</b></span><span class="legend-h2"><i></i>H2 <b
|
||||
id="metric-ws-proto-h2">0</b></span><span class="legend-h3"><i></i>H3 <b
|
||||
id="metric-ws-proto-h3">0</b></span></small></article>
|
||||
<article class="stat" data-dashboard-widget="stream_overview" hidden>
|
||||
<span data-i18n="streamWidgetOverview">Stream overview</span><strong id="stream-dashboard-overview">-</strong>
|
||||
<small id="stream-dashboard-overview-detail" data-i18n="streamWidgetUnavailable">No stream
|
||||
data</small>
|
||||
</article>
|
||||
<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 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>
|
||||
<polyline class="diagnostic-line stream-chart-tx" id="stream-dashboard-tx" points="0,55 300,55"></polyline>
|
||||
</svg><small class="stat-chart-legend stream-traffic-legend" id="stream-dashboard-traffic-detail">
|
||||
<span class="legend-rx"><i></i>RX <b id="stream-dashboard-rx-rate">-</b></span>
|
||||
<span class="legend-tx"><i></i>TX <b id="stream-dashboard-tx-rate">-</b></span>
|
||||
</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="stream_sessions" hidden>
|
||||
<span data-i18n="streamWidgetSessions">Stream sessions</span><strong id="stream-dashboard-sessions">-</strong>
|
||||
<small id="stream-dashboard-sessions-detail" data-i18n="streamWidgetUnavailable">No stream
|
||||
data</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="stream_backend_health" hidden>
|
||||
<span data-i18n="streamWidgetHealth">Stream backend health</span><strong id="stream-dashboard-health">-</strong>
|
||||
<small id="stream-dashboard-health-detail" data-i18n="streamWidgetUnavailable">No stream
|
||||
data</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_health" hidden>
|
||||
<span data-i18n="backendWidgetHealth">Backend health</span><strong id="backend-dashboard-health">-</strong>
|
||||
<small id="backend-dashboard-health-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="backend-dashboard-activity-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="backend-dashboard-errors-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="backend-dashboard-latency-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
<polyline class="diagnostic-line stream-chart-tx" id="backend-dashboard-out-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small class="stat-chart-legend stream-traffic-legend">
|
||||
<span class="legend-rx"><i></i>IN <b id="backend-dashboard-in-rate">-</b></span>
|
||||
<span class="legend-tx"><i></i>OUT <b id="backend-dashboard-out-rate">-</b></span>
|
||||
</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_protocols" hidden>
|
||||
<span data-i18n="backendWidgetProtocols">Configured backend modes</span><strong
|
||||
id="backend-dashboard-protocols">-</strong>
|
||||
<div class="backend-mode-bar" role="img" data-i18n-aria="backendWidgetProtocols">
|
||||
<span class="backend-mode-h1" id="backend-mode-h1"></span>
|
||||
<span class="backend-mode-auto" id="backend-mode-auto"></span>
|
||||
<span class="backend-mode-compat" id="backend-mode-compat"></span>
|
||||
<span class="backend-mode-h2" id="backend-mode-h2"></span>
|
||||
</div><small id="backend-dashboard-protocols-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="backend-dashboard-requests-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="backend_capacity" hidden>
|
||||
<span data-i18n="backendWidgetCapacity">Backend capacity</span><strong id="backend-dashboard-capacity">-</strong>
|
||||
<div class="overview-meter" id="backend-dashboard-capacity-meter" role="progressbar"
|
||||
data-i18n-aria="backendWidgetCapacity" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="backend-dashboard-capacity-meter-fill"></span>
|
||||
</div><small id="backend-dashboard-capacity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden>
|
||||
<span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><strong id="backend-dashboard-http5xx">-</strong>
|
||||
<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>
|
||||
</svg><small id="backend-dashboard-http5xx-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_timeouts" hidden>
|
||||
<span data-i18n="backendWidgetTimeouts">backendWidgetTimeouts</span><strong
|
||||
id="backend-dashboard-timeouts">-</strong>
|
||||
<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>
|
||||
</svg><small id="backend-dashboard-timeouts-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_network" hidden>
|
||||
<span data-i18n="backendWidgetNetwork">backendWidgetNetwork</span><strong id="backend-dashboard-network">-</strong>
|
||||
<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>
|
||||
</svg><small id="backend-dashboard-network-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_retries" hidden>
|
||||
<span data-i18n="backendWidgetRetries">backendWidgetRetries</span><strong id="backend-dashboard-retries">-</strong>
|
||||
<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>
|
||||
</svg><small id="backend-dashboard-retries-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_queue" hidden>
|
||||
<span data-i18n="backendWidgetQueue">backendWidgetQueue</span><strong id="backend-dashboard-queue">-</strong>
|
||||
<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>
|
||||
</svg><small id="backend-dashboard-queue-detail">-</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_rotations" hidden>
|
||||
<span data-i18n="backendWidgetRotations">Backend client pool rotations</span><strong
|
||||
id="backend-dashboard-rotations">-</strong>
|
||||
<small id="backend-dashboard-rotations-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_process_cpu-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_process_rss-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_process_fds" hidden>
|
||||
<span data-i18n="overviewExtraProcessFds">overviewExtraProcessFds</span><strong
|
||||
id="overview-extra-extra_process_fds">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_process_fds-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraProcessFds" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_process_fds-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_process_fds-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_open_connections-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_websocket_active-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_tls_error_rate-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_request_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="extra_cache_hit_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheHitRate">Cache hits share (recent)</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="extra-cache-ratio-chart" role="img"
|
||||
aria-label="Recent cache HIT and MISS distribution" viewBox="0 0 64 64">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="overview-donut-hit" id="extra-cache-ratio-hit-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
<circle class="overview-donut-miss" id="extra-cache-ratio-miss-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
</svg>
|
||||
<div class="overview-donut-info">
|
||||
<strong id="overview-extra-extra_cache_hit_rate">-</strong>
|
||||
<small class="overview-donut-legend" id="overview-extra-extra_cache_hit_rate-detail">
|
||||
<span class="overview-donut-hit-key"><i></i>HIT <b id="extra-cache-ratio-hit-count">0</b></span>
|
||||
<span class="overview-donut-miss-key"><i></i>MISS <b id="extra-cache-ratio-miss-count">0</b></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cache_memory">
|
||||
<span data-i18n="overviewExtraCacheMemory">overviewExtraCacheMemory</span><strong
|
||||
id="overview-extra-extra_cache_memory">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cache_memory-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraCacheMemory" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_cache_memory-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_cache_memory-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cold_queue_memory">
|
||||
<span data-i18n="overviewExtraColdQueueMemory">COLD queue RAM utilization</span>
|
||||
<strong id="overview-extra-extra_cold_queue_memory">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cold_queue_memory-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraColdQueueMemory" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_cold_queue_memory-meter-fill"></span>
|
||||
</div>
|
||||
<small id="overview-extra-extra_cold_queue_memory-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cold_storage">
|
||||
<span data-i18n="overviewExtraColdStorage">COLD disk utilization</span>
|
||||
<strong id="overview-extra-extra_cold_storage">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cold_storage-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraColdStorage" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_cold_storage-meter-fill"></span>
|
||||
</div>
|
||||
<small id="overview-extra-extra_cold_storage-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h2_share" hidden>
|
||||
<span data-i18n="overviewExtraH2Share">overviewExtraH2Share</span><strong
|
||||
id="overview-extra-extra_h2_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h2_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH2Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h2_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h2_share-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_response_latency-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_backend_unhealthy-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_total_rps-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_active_exchanges-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h1_share" hidden>
|
||||
<span data-i18n="overviewExtraH1Share">HTTP/1.1 share</span><strong id="overview-extra-extra_h1_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h1_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH1Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h1_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h1_share-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h3_share" hidden>
|
||||
<span data-i18n="overviewExtraH3Share">HTTP/3 share</span><strong id="overview-extra-extra_h3_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h3_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH3Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h3_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h3_share-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_cache_miss_rate-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg><small id="overview-extra-extra_backend_active-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<!-- RP_METRICS_SPLIT -->
|
||||
<article class="stat stat-donut" data-dashboard-widget="www_status" hidden>
|
||||
<span data-i18n="webMetricStatus">webMetricStatus</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="www_status-chart" role="img" viewBox="0 0 64 64" aria-label="WWW status"
|
||||
data-i18n-aria="webMetricStatus">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="www-ring-arc www-ring-0" id="www_status-arc-0" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-1" id="www_status-arc-1" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-2" id="www_status-arc-2" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-3" id="www_status-arc-3" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
</svg>
|
||||
<div class="overview-donut-info"><strong id="www_status-value">-</strong><small
|
||||
class="overview-donut-legend www-ring-legend"><span><i class="www-dot-0"></i>2xx
|
||||
<b id="www_status-count-0">0</b></span>
|
||||
<span><i class="www-dot-1"></i>3xx <b id="www_status-count-1">0</b></span>
|
||||
<span><i class="www-dot-2"></i>4xx <b id="www_status-count-2">0</b></span>
|
||||
<span><i class="www-dot-3"></i>5xx <b id="www_status-count-3">0</b></span></small></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="www_kind" hidden>
|
||||
<span data-i18n="webMetricKind">webMetricKind</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="www_kind-chart" role="img" viewBox="0 0 64 64" aria-label="Static / PHP"
|
||||
data-i18n-aria="webMetricKind">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="www-ring-arc www-ring-0" id="www_kind-arc-0" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-1" id="www_kind-arc-1" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-2" id="www_kind-arc-2" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
</svg>
|
||||
<div class="overview-donut-info"><strong id="www_kind-value">-</strong><small
|
||||
class="overview-donut-legend www-ring-legend"><span><i class="www-dot-0"></i>Static <b
|
||||
id="www_kind-count-0">0</b></span>
|
||||
<span><i class="www-dot-1"></i>PHP <b id="www_kind-count-1">0</b></span>
|
||||
<span><i class="www-dot-2"></i>Other <b id="www_kind-count-2">0</b></span></small></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="www_capacity" hidden>
|
||||
<span data-i18n="webMetricCapacity">webMetricCapacity</span><strong id="www_capacity-value">-</strong>
|
||||
<div class="overview-meter" id="www_capacity-meter" role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="www_capacity-meter-fill"></span>
|
||||
</div><small id="www_capacity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="www_error_rate" hidden>
|
||||
<span data-i18n="webMetricErrorRate">webMetricErrorRate</span><strong id="www_error_rate-value">-</strong>
|
||||
<div class="overview-meter" id="www_error_rate-meter" role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="www_error_rate-meter-fill"></span>
|
||||
</div><small id="www_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="www_methods" hidden>
|
||||
<span data-i18n="webMetricMethods">HTTP Methods</span><strong id="www_methods-value">-</strong>
|
||||
<div class="www-segmented" id="www_methods-bar" role="img" aria-label="HTTP method share"
|
||||
data-i18n-aria="webMetricMethods"><span class="www-segment-0" id="www_methods-segment-0"></span><span
|
||||
class="www-segment-1" id="www_methods-segment-1"></span><span class="www-segment-2"
|
||||
id="www_methods-segment-2"></span><span class="www-segment-3" id="www_methods-segment-3"></span><span
|
||||
class="www-segment-4" id="www_methods-segment-4"></span></div>
|
||||
<small class="www-method-legend" id="www_methods-detail">-</small>
|
||||
</article>
|
||||
<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 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>
|
||||
</svg>
|
||||
<small data-i18n="webMetricPerSecond">Requests per second</small>
|
||||
</article>
|
||||
+347
-2
@@ -3508,6 +3508,10 @@ body[data-density=dense] .stat strong {
|
||||
margin: 6px 0
|
||||
}
|
||||
|
||||
#panel-cache>article.card+article.card {
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.cache-table {
|
||||
font-size: 12px
|
||||
}
|
||||
@@ -11210,7 +11214,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
border-radius: var(--rp-radius-surface) !important;
|
||||
}
|
||||
|
||||
html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-style=rounded], [data-corner-style=extra]) :is(button:not(.nav):not(.file-log-tab), input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea, .pill, .vhost-backend-protocol-badge, .overview-widget-type, .badge, .quiet, .outline, .primary, .code, .command, .dashboard-widget-toggle, .settings-tabs button, .overview-inline-hidden-toggle, .overview-inline-restore) {
|
||||
html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-style=rounded], [data-corner-style=extra]) :is(button:not(.nav):not(.file-log-tab):not(.vhost-backend-trigger), input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea, .pill, .vhost-backend-protocol-badge, .overview-widget-type, .badge, .quiet, .outline, .primary, .code, .command, .dashboard-widget-toggle, .settings-tabs button, .overview-inline-hidden-toggle, .overview-inline-restore) {
|
||||
border-radius: var(--rp-radius-control) !important;
|
||||
}
|
||||
|
||||
@@ -11237,6 +11241,16 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
padding: 3px 7px;
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.vhost-backend-protocol-badge:is(:hover, [aria-expanded=true]) {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.vhost-backend-protocol-badge:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vhost-backend-protocol-badge[data-observed=true] {
|
||||
@@ -11596,7 +11610,6 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Web Services reuse the same full-width live tiles and chart as Vhosts. */
|
||||
#panel-web-server .www-service-panel {
|
||||
min-width: 0;
|
||||
margin-top: 10px;
|
||||
@@ -11813,4 +11826,336 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] {
|
||||
padding-top: 135px;
|
||||
}
|
||||
}
|
||||
|
||||
#panel-overview :is(.system-card, .connection-card) :is(.card-heading h2, .card-heading .meta, .memory-line, .chart-footer, .connection-legend) {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview :is(.system-card, .connection-card) .card-heading h2 {
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.overview-presets-dialog {
|
||||
width: calc(min(730px, 100vw - 24px));
|
||||
max-height: calc(100dvh - 24px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.overview-presets-content {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.overview-presets-content>p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.overview-presets-create {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.overview-presets-create-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.overview-presets-create-controls input {
|
||||
min-width: 150px;
|
||||
flex: 1 1 240px;
|
||||
}
|
||||
|
||||
.overview-presets-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.overview-presets-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius, 8px);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.overview-presets-row strong {
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.overview-presets-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.overview-presets-dialog .overview-layout-footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media(max-width:480px) {
|
||||
.overview-presets-create-controls>button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.overview-presets-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview-presets-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#panel-overview .activity-card .card-heading h2,
|
||||
#panel-overview .overview-wide-chart .card-heading h2,
|
||||
#panel-overview :is(.stat-chart, .stat-donut)>span:first-child,
|
||||
#panel-overview :is(.system-card, .connection-card) .overview-chart-heading,
|
||||
.function-live-chart-heading>strong,
|
||||
.mini-live-chart>.mini-live-chart-head>span:first-child,
|
||||
.traffic-chart-card>h3.subheading,
|
||||
.vhost-chart-head>span:first-child {
|
||||
font-family: var(--sans);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
letter-spacing: normal;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.activity-card .card-heading h2,
|
||||
html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.overview-wide-chart .card-heading h2,
|
||||
html[data-dashboard-density] #panel-overview .stat:is(.stat-chart, .stat-donut)>span:first-child {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#panel-overview :is(.system-card, .connection-card) .overview-chart-heading {
|
||||
color: var(--text);
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
:is(.overview-wide-summary, .overview-wide-legend, .overview-donut-legend, .www-method-legend, .stat-chart-legend, .mini-chart-legend, .connection-legend, .traffic-protocol-legend, .traffic-chart-legend, .function-live-chart-legend, .stream-chart-legend, .chart-footer, .vhost-chart-head, .network-interface-rate) {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview .overview-wide-chart>.fine,
|
||||
#panel-overview .stat-chart>small,
|
||||
#panel-overview .www-top-row,
|
||||
.traffic-bar-row {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview :is(.overview-wide-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stat-chart-legend, .mini-chart-legend, .connection-legend, .traffic-protocol-legend, .stream-traffic-legend),
|
||||
:is(.overview-wide-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stat-chart-legend, .mini-chart-legend, .connection-legend, .traffic-protocol-legend, .traffic-chart-legend, .function-live-chart-legend, .stream-chart-legend, .stream-traffic-legend) {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview :is(.overview-wide-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stat-chart-legend, .mini-chart-legend, .connection-legend, .traffic-protocol-legend, .stream-traffic-legend) :is(span, b, strong, small, li, em),
|
||||
:is(.overview-wide-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stat-chart-legend, .mini-chart-legend, .connection-legend, .traffic-protocol-legend, .traffic-chart-legend, .function-live-chart-legend, .stream-chart-legend, .stream-traffic-legend) :is(span, b, strong, small, li, em) {
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stream-traffic-legend),
|
||||
html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stream-traffic-legend) :is(span, b, small, strong, li) {
|
||||
font-size: 9px;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.vhost-detail.vhost-backend-trigger,
|
||||
.vhost-live-metric.vhost-backend-trigger {
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vhost-detail.vhost-backend-trigger {
|
||||
border: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.vhost-live-metric.vhost-backend-trigger {
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vhost-backend-trigger:is(:hover, [aria-expanded=true]) {
|
||||
background: var(--panel2);
|
||||
}
|
||||
|
||||
.vhost-backend-trigger:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup {
|
||||
position: fixed;
|
||||
z-index: 1600;
|
||||
max-height: calc(min(360px, 100dvh - 24px));
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--rp-radius-surface, 0);
|
||||
box-shadow: 0 12px 35px var(--shadow-strong);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup h4 {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-close {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font: 18px/1 var(--sans);
|
||||
border-radius: var(--rp-radius-control, 0);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-close:hover {
|
||||
background: var(--panel2);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-row {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--rp-radius-surface, 0);
|
||||
background: var(--bg);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-name {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
overflow-wrap: anywhere;
|
||||
font: 650 11px var(--mono);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-columns {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
|
||||
gap: 6px 10px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field>span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field>span:first-child {
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field>span:last-child {
|
||||
font: 10px/1.35 var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field.has-success>span:last-child {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field.has-error>span:last-child {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-hint {
|
||||
margin: 10px 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>.card .mini-heading>h3,
|
||||
#panel-overview .dashboard-overview-grid>.card .overview-chart-heading,
|
||||
#panel-overview .dashboard-overview-grid>.card:is(.overview-wide-chart, .activity-card, .pool-card) .card-heading h2,
|
||||
#panel-overview .dashboard-overview-grid>.stat>span:first-child {
|
||||
font-family: var(--sans);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>[data-dashboard-widget] :is(small, .mini-metrics dt, .mini-metrics dd, .mini-live-chart-head, .memory-line, .chart-footer, .small-label, .meta, .fine) {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid .mini-live-chart>.mini-live-chart-head>span:first-child {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.pool-card .card-heading h2 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#panel-cache>.card .card-heading h2,
|
||||
#panel-overview .dashboard-overview-grid .mini-live-chart-head>span:first-child,
|
||||
#panel-overview .dashboard-overview-grid>.card .mini-heading>h3,
|
||||
#panel-overview .dashboard-overview-grid>.card .overview-chart-heading,
|
||||
#panel-overview .dashboard-overview-grid>.card:not(.system-card):not(.connection-card) .card-heading h2,
|
||||
#panel-overview .dashboard-overview-grid>.stat>span:first-child,
|
||||
#panel-transform>.card .card-heading h2,
|
||||
.function-live-chart-heading>strong,
|
||||
.traffic-chart-card>h3.subheading,
|
||||
.vhost-chart-head>span:first-child {
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -1,3 +1,158 @@
|
||||
// Shared, anchored Vhost popover for backend quick-view and upstream HTTP
|
||||
// protocol observations. Both consume existing live status without extra probes.
|
||||
let activeVhostBackendPopup = null;
|
||||
function closeVhostBackendPopup(returnFocus = false) {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
activeVhostBackendPopup = null;
|
||||
current.trigger.setAttribute("aria-expanded", "false");
|
||||
current.node.remove();
|
||||
if (returnFocus && current.trigger.isConnected) current.trigger.focus();
|
||||
}
|
||||
function positionVhostBackendPopup() {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
const rect = current.trigger.getBoundingClientRect(), edge = 12, gap = 6;
|
||||
const width = Math.min(430, Math.max(0, window.innerWidth - edge * 2));
|
||||
const height = Math.min(current.node.scrollHeight, 360, Math.max(100, window.innerHeight - edge * 2));
|
||||
const below = window.innerHeight - rect.bottom - edge - gap;
|
||||
const above = rect.top - edge - gap;
|
||||
const placeAbove = below < height && above > below;
|
||||
const desiredTop = placeAbove ? rect.top - height - gap : rect.bottom + gap;
|
||||
current.node.style.width = `${width}px`;
|
||||
current.node.style.left = `${Math.max(edge, Math.min(rect.left, window.innerWidth - width - edge))}px`;
|
||||
current.node.style.top = `${Math.max(edge, Math.min(desiredTop, window.innerHeight - height - edge))}px`;
|
||||
}
|
||||
function vhostPopupHeading(label) {
|
||||
const heading = el("h4", label);
|
||||
heading.id = "vhost-backend-popup-heading";
|
||||
const headingRow = el("div", undefined, "vhost-backend-popup-heading");
|
||||
const closeButton = el("button", "×", "vhost-backend-popup-close");
|
||||
closeButton.type = "button";
|
||||
closeButton.setAttribute("aria-label", t("close"));
|
||||
closeButton.addEventListener("click", () => closeVhostBackendPopup(true));
|
||||
headingRow.append(heading, closeButton);
|
||||
return { heading, headingRow };
|
||||
}
|
||||
function refreshVhostProtocolPopup(current, vhost, members) {
|
||||
const backend = (vhost.backends || []).find(item => item.id === current.backendId);
|
||||
if (!backend) { closeVhostBackendPopup(); return; }
|
||||
const member = members.get(backend.id);
|
||||
const observed = upstreamProtocolBadgeText(backend, member);
|
||||
if (!current.protocolView) {
|
||||
const { heading, headingRow } = vhostPopupHeading(observed.label);
|
||||
const info = el("p", "", "fine vhost-backend-popup-hint");
|
||||
const list = el("ul", undefined, "vhost-backend-popup-list");
|
||||
const row = el("li", undefined, "vhost-backend-popup-row");
|
||||
const columns = el("div", undefined, "vhost-backend-popup-columns");
|
||||
const counters = [];
|
||||
for (const protocol of ["H1", "H2"]) {
|
||||
const field = el("div", undefined, "vhost-backend-popup-field");
|
||||
const value = el("span", "0");
|
||||
field.append(el("span", protocol), value);
|
||||
columns.append(field); counters.push(value);
|
||||
}
|
||||
row.append(columns); list.append(row);
|
||||
current.node.append(headingRow, info, list);
|
||||
current.node.setAttribute("aria-labelledby", heading.id);
|
||||
current.protocolView = { heading, info, list, counters };
|
||||
}
|
||||
const view = current.protocolView;
|
||||
const explanation = t(observed.recent ? "vhBackendProtocolRecent" : "vhBackendProtocolUnobserved");
|
||||
if (view.heading.textContent !== observed.label) view.heading.textContent = observed.label;
|
||||
if (view.info.textContent !== explanation) view.info.textContent = explanation;
|
||||
view.list.hidden = !observed.recent;
|
||||
if (observed.recent) {
|
||||
const totals = [member.upstream_protocol_http1_total || 0, member.upstream_protocol_http2_total || 0];
|
||||
totals.forEach((total, index) => {
|
||||
const label = String(total);
|
||||
if (view.counters[index].textContent !== label) view.counters[index].textContent = label;
|
||||
});
|
||||
}
|
||||
positionVhostBackendPopup();
|
||||
}
|
||||
function refreshVhostBackendPopup(status = latestStatus) {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
const vhost = (envelope?.config.vhosts || []).find(item => item.id === current.id);
|
||||
if (!vhost || !current.trigger.isConnected) { closeVhostBackendPopup(); return; }
|
||||
const pool = (status?.backend_pools || []).find(item => item.id === vhost.id);
|
||||
const members = new Map((pool?.members || []).map(member => [member.id, member]));
|
||||
if (current.backendId !== null) {
|
||||
refreshVhostProtocolPopup(current, vhost, members);
|
||||
return;
|
||||
}
|
||||
const { heading, headingRow } = vhostPopupHeading(`${t("vhBackendPopupTitle")} · ${vhost.id}`);
|
||||
const list = el("ul", undefined, "vhost-backend-popup-list");
|
||||
for (const backend of vhost.backends || []) {
|
||||
const member = members.get(backend.id);
|
||||
const row = el("li", undefined, "vhost-backend-popup-row");
|
||||
row.append(el("strong", backend.id, "vhost-backend-popup-name"));
|
||||
let address = "-", scheme = "-";
|
||||
try {
|
||||
const url = new URL(backend.url);
|
||||
scheme = url.protocol.replace(":", "").toUpperCase();
|
||||
const port = url.port || (scheme === "HTTPS" ? "443" : scheme === "HTTP" ? "80" : "");
|
||||
address = `${url.hostname}${port ? `:${port}` : ""}`;
|
||||
} catch { /* Configuration may have changed while status was refreshing. */ }
|
||||
const upstream = upstreamProtocolBadgeText(backend, member);
|
||||
const protocol = `${scheme} · ${upstream.mode}${upstream.observed ? ` → ${upstream.observed}` : ""}`;
|
||||
const statusCode = Number(member?.last_status);
|
||||
const hasStatus = member && Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599;
|
||||
const httpResponse = hasStatus ? `HTTP ${statusCode}` : t("vhBackendPopupNoResponse");
|
||||
const statusLabel = !member ? t("vhHealthUnknown") : t(({
|
||||
up: "vhUp", down: "vhDown", disabled: "vhDisabled", checking: "vhChecking", draining: "vhDraining"
|
||||
})[member.status] || "vhHealthUnknown");
|
||||
const columns = el("div", undefined, "vhost-backend-popup-columns");
|
||||
for (const [key, value] of [
|
||||
["vhBackendPopupAddress", address], ["vhBackendPopupProtocol", protocol],
|
||||
["vhBackendPopupLastHttp", httpResponse], ["status", statusLabel]
|
||||
]) {
|
||||
const field = el("div", undefined, "vhost-backend-popup-field");
|
||||
field.append(text("span", key), el("span", value));
|
||||
if (key === "vhBackendPopupLastHttp" && hasStatus) field.classList.add(statusCode >= 500 ? "has-error" : statusCode < 400 ? "has-success" : "");
|
||||
columns.append(field);
|
||||
}
|
||||
row.append(columns); list.append(row);
|
||||
}
|
||||
if (!list.children.length) list.append(el("li", t("vhBackendPopupEmpty"), "fine"));
|
||||
const foot = el("p", t("vhBackendPopupHint"), "fine vhost-backend-popup-hint");
|
||||
current.node.replaceChildren(headingRow, list, foot);
|
||||
current.node.setAttribute("aria-labelledby", heading.id);
|
||||
positionVhostBackendPopup();
|
||||
}
|
||||
function toggleVhostBackendPopup(id, trigger, backendId = null) {
|
||||
if (activeVhostBackendPopup?.trigger === trigger) { closeVhostBackendPopup(); return; }
|
||||
closeVhostBackendPopup();
|
||||
const node = el("section", undefined, "vhost-backend-popup");
|
||||
node.id = "vhost-backend-popup";
|
||||
node.setAttribute("role", "dialog");
|
||||
node.setAttribute("aria-modal", "false");
|
||||
trigger.setAttribute("aria-expanded", "true");
|
||||
activeVhostBackendPopup = { id, trigger, node, backendId, protocolView: null };
|
||||
document.body.append(node);
|
||||
refreshVhostBackendPopup();
|
||||
}
|
||||
function enableVhostBackendPopup(trigger, vhost, backendId = null) {
|
||||
trigger.dataset.vhostBackendTrigger = vhost.id;
|
||||
trigger.setAttribute("aria-haspopup", "dialog");
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
trigger.setAttribute("aria-controls", "vhost-backend-popup");
|
||||
if (backendId === null) trigger.setAttribute("aria-label", `${t("vhBackendPopupOpen")}: ${vhost.id}`);
|
||||
trigger.addEventListener("click", () => toggleVhostBackendPopup(vhost.id, trigger, backendId));
|
||||
}
|
||||
document.addEventListener("pointerdown", event => {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (current && !current.node.contains(event.target) && !current.trigger.contains(event.target)) closeVhostBackendPopup();
|
||||
});
|
||||
document.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape" && activeVhostBackendPopup) { event.preventDefault(); event.stopPropagation(); closeVhostBackendPopup(true); }
|
||||
});
|
||||
window.addEventListener("resize", positionVhostBackendPopup);
|
||||
document.addEventListener("scroll", event => {
|
||||
if (activeVhostBackendPopup && !activeVhostBackendPopup.node.contains(event.target)) positionVhostBackendPopup();
|
||||
}, true);
|
||||
|
||||
const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
|
||||
const vhostSortColumns = ["id", "hostname", "status", "listeners", "certificate", "backends"];
|
||||
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] });
|
||||
@@ -107,7 +262,8 @@ function upstreamProtocolBadgeText(backend, member, now = Date.now()) {
|
||||
const seen = Number(member?.upstream_protocol_last_seen_ms);
|
||||
const recent = member && (member.upstream_protocol_observed === "h1" || member.upstream_protocol_observed === "h2") &&
|
||||
Number.isFinite(seen) && seen > 0 && now >= seen && now - seen <= VHOST_PROTOCOL_FRESH_MS;
|
||||
return { label: `${backend.id} · ${mode} → ${recent ? member.upstream_protocol_observed.toUpperCase() : "?"}`, recent };
|
||||
const observed = recent ? member.upstream_protocol_observed.toUpperCase() : null;
|
||||
return { label: `${backend.id} · ${mode} → ${observed || "?"}`, recent, mode, observed };
|
||||
}
|
||||
function renderUpstreamProtocolBadges(status) {
|
||||
const members = new Map((status.backend_pools || []).flatMap(pool => (pool.members || []).map(member =>
|
||||
@@ -119,13 +275,12 @@ function renderUpstreamProtocolBadges(status) {
|
||||
if (!backend) return;
|
||||
const member = members.get(`${vhost}\u0000${id}`);
|
||||
const observed = upstreamProtocolBadgeText(backend, member, now);
|
||||
node.textContent = observed.label;
|
||||
if (node.textContent !== observed.label) node.textContent = observed.label;
|
||||
node.dataset.observed = String(!!observed.recent);
|
||||
const counts = member && observed.recent ? ` H1: ${member.upstream_protocol_http1_total || 0}; H2: ${member.upstream_protocol_http2_total || 0}.` : "";
|
||||
node.title = `${t(observed.recent ? "vhBackendProtocolRecent" : "vhBackendProtocolUnobserved")}${counts}`;
|
||||
});
|
||||
}
|
||||
function render() {
|
||||
closeVhostBackendPopup();
|
||||
if (!envelope) return;
|
||||
if (!initialized) load(defaults(), [], false);
|
||||
const config = envelope.config;
|
||||
@@ -209,10 +364,21 @@ function render() {
|
||||
: t("webServer"))
|
||||
: `${(v.backends || []).length}`],
|
||||
];
|
||||
for (const [key, value] of detailItems) { const item = el("div", undefined, "vhost-detail"); item.append(text("span", key, "small-label"), el("b", value)); details.append(item); }
|
||||
for (const [key, value] of detailItems) {
|
||||
const isBackend = key === "vhBackendPool";
|
||||
const item = el(isBackend ? "button" : "div", undefined, `vhost-detail${isBackend ? " vhost-backend-trigger" : ""}`);
|
||||
if (isBackend) { item.type = "button"; enableVhostBackendPopup(item, v); }
|
||||
item.append(text("span", key, "small-label"), el("b", value)); details.append(item);
|
||||
}
|
||||
box.append(details);
|
||||
const live = el("div", undefined, "vhost-live-grid");
|
||||
const metric = (key, suffix) => { const item = el("div", undefined, "vhost-live-metric"); item.append(text("span", key), el("strong", "-")); item.querySelector("strong").dataset[suffix] = v.id; live.append(item); };
|
||||
const metric = (key, suffix) => {
|
||||
const isHealth = key === "status";
|
||||
const item = el(isHealth ? "button" : "div", undefined, `vhost-live-metric${isHealth ? " vhost-backend-trigger" : ""}`);
|
||||
if (isHealth) { item.type = "button"; enableVhostBackendPopup(item, v); }
|
||||
item.append(text("span", key), el("strong", "-"));
|
||||
item.querySelector("strong").dataset[suffix] = v.id; live.append(item);
|
||||
};
|
||||
metric("status", "vhostHealth"); metric("vhRequests", "vhostRequests"); metric("vhActive", "vhostActive"); metric("vhErrors", "vhostErrors"); metric("vhLatency", "vhostLatency"); metric("vhTraffic", "vhostTraffic");
|
||||
box.append(live);
|
||||
const chart = el("div", undefined, "vhost-chart"); const chartHead = el("div", undefined, "vhost-chart-head"); chartHead.append(text("span", "vhLifetimeChart", "small-label")); const chartValue = el("span", "-", "fine"); chartValue.dataset.chartValueFor = v.id; chartHead.append(chartValue); chart.append(chartHead);
|
||||
@@ -243,11 +409,12 @@ function render() {
|
||||
// Retain their own badge class so observed H1/H2 keeps its green styling.
|
||||
if (hasProxySource) {
|
||||
for (const backend of v.backends || []) {
|
||||
const badge = el("span", undefined, "vhost-backend-protocol-badge");
|
||||
const badge = el("button", undefined, "vhost-backend-protocol-badge");
|
||||
badge.type = "button";
|
||||
badge.dataset.upstreamProtocolVhost = v.id;
|
||||
badge.dataset.upstreamProtocolBackend = backend.id;
|
||||
badge.textContent = upstreamProtocolBadgeText(backend, null).label;
|
||||
badge.title = t("vhBackendProtocolUnobserved");
|
||||
enableVhostBackendPopup(badge, v, backend.id);
|
||||
policy.append(badge);
|
||||
}
|
||||
}
|
||||
@@ -282,6 +449,7 @@ function vhostTrafficRate(id, metrics, snapshot, now, seen) {
|
||||
}
|
||||
function renderStatus(status) {
|
||||
renderUpstreamProtocolBadges(status);
|
||||
refreshVhostBackendPopup(status);
|
||||
form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.());
|
||||
const rows = [], summaries = new Map(), aggregate = new Map();
|
||||
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set();
|
||||
@@ -415,5 +583,5 @@ $("pool-open").addEventListener("click", () => setPanel("vhosts"));
|
||||
initPoolTableTools();
|
||||
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
|
||||
window.rustProxyDrafts.register("vhost", { labelKey: "vhosts", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { closeVhostBackendPopup(); dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
@@ -2,6 +2,7 @@
|
||||
//! No database call is made from the public HTTP forwarding path.
|
||||
use crate::config::{Config, Envelope};
|
||||
mod certificate_api;
|
||||
mod dashboard_presets;
|
||||
mod history;
|
||||
use anyhow::{ensure, Context, Result};
|
||||
pub use history::{ConfigurationHistoryDetail, ConfigurationRevision};
|
||||
@@ -12,7 +13,7 @@ use sqlx::{
|
||||
};
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
pub const SCHEMA_VERSION: i64 = 9;
|
||||
pub const SCHEMA_VERSION: i64 = 10;
|
||||
pub const CERTIFICATE_API_CREDENTIAL_CAPACITY: i64 = 32;
|
||||
pub const AUDIT_CAPACITY: i64 = 10_000;
|
||||
|
||||
@@ -123,6 +124,9 @@ impl Store {
|
||||
))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::raw_sql(include_str!("../migrations/0010_dashboard_presets.sql"))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("INSERT OR IGNORE INTO configuration(id, revision, document) VALUES(1, 0, ?)")
|
||||
.bind(serde_json::to_string(initial)?)
|
||||
.execute(&mut *tx)
|
||||
@@ -133,7 +137,7 @@ impl Store {
|
||||
.bind(crate::auth::now())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("PRAGMA user_version = 9")
|
||||
sqlx::query("PRAGMA user_version = 10")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! SQLite persistence for user-named Overview snapshots (control plane only).
|
||||
use super::Store;
|
||||
use anyhow::{ensure, Result};
|
||||
use sqlx::Row;
|
||||
|
||||
impl Store {
|
||||
pub async fn list_admin_dashboard_presets(
|
||||
&self,
|
||||
username: &str,
|
||||
) -> Result<Vec<(String, String)>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT p.name, p.document FROM dashboard_presets p \
|
||||
JOIN administrator a ON a.id = p.administrator_id \
|
||||
WHERE a.id = 1 AND a.username = ? ORDER BY p.name COLLATE NOCASE",
|
||||
)
|
||||
.bind(username)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
rows.iter()
|
||||
.map(|row| Ok((row.try_get("name")?, row.try_get("document")?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn save_admin_dashboard_preset(
|
||||
&self,
|
||||
username: &str,
|
||||
name: &str,
|
||||
document: &str,
|
||||
) -> Result<bool> {
|
||||
ensure!(
|
||||
document.len() >= 2 && document.len() <= 8192,
|
||||
"invalid dashboard preset size"
|
||||
);
|
||||
// Limit to 16 user-named presets. The cap and upsert are checked in one
|
||||
// SQLite statement, including under concurrent requests.
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO dashboard_presets(administrator_id, name, document, updated_at) \
|
||||
SELECT a.id, ?, ?, ? FROM administrator a WHERE a.id = 1 AND a.username = ? \
|
||||
AND ((SELECT count(*) FROM dashboard_presets WHERE administrator_id = a.id) < 16 \
|
||||
OR EXISTS(SELECT 1 FROM dashboard_presets WHERE administrator_id = a.id AND name = ?)) \
|
||||
ON CONFLICT(administrator_id, name) DO UPDATE SET \
|
||||
document = excluded.document, updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(name).bind(document).bind(crate::auth::now())
|
||||
.bind(username).bind(name)
|
||||
.execute(&self.pool).await?;
|
||||
Ok(result.rows_affected() == 1)
|
||||
}
|
||||
|
||||
pub async fn delete_admin_dashboard_preset(&self, username: &str, name: &str) -> Result<bool> {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM dashboard_presets WHERE administrator_id = \
|
||||
(SELECT id FROM administrator WHERE id = 1 AND username = ?) AND name = ?",
|
||||
)
|
||||
.bind(username)
|
||||
.bind(name)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() == 1)
|
||||
}
|
||||
|
||||
pub async fn rename_admin_dashboard_preset(
|
||||
&self,
|
||||
username: &str,
|
||||
old: &str,
|
||||
new: &str,
|
||||
) -> Result<bool> {
|
||||
// UPDATE OR IGNORE ensures an existing target name is never overwritten.
|
||||
let result = sqlx::query(
|
||||
"UPDATE OR IGNORE dashboard_presets SET name = ?, updated_at = ? \
|
||||
WHERE administrator_id = (SELECT id FROM administrator WHERE id = 1 AND username = ?) \
|
||||
AND name = ? AND name <> ?",
|
||||
)
|
||||
.bind(new)
|
||||
.bind(crate::auth::now())
|
||||
.bind(username)
|
||||
.bind(old)
|
||||
.bind(new)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() == 1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user