public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host
@ 2026-04-13  8:58 Lukas Wagner
  2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 01/11] node status panel: add `children` property Lukas Wagner
                   ` (10 more replies)
  0 siblings, 11 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

This series add metric collection physical PDM hosts.

The patches for proxmox-yew-comp slight adapt the existing NodeStatusPanel to allow the application
to inject child components into the same panel.

The proxmox-datacenter-manager patches do some initial refactoring (naming), and then add the needed
collection loop, API types and UI elements.

Changes since v2:
  - rebased
  - adapted PDM part to changes in proxmox-disks (minor, only renaming)
  - drop already applied patches for 'proxmox'
  - drop patches for 'proxmox-backup' - these need to be adapted for the refactoring of
    proxmox-disks and are pretty much independent of this series here, so they will be posted
    separately

Changes since v1:
  - rebased
  - fixed failing unit test
  - fixed mistake in Cargo.toml
  - dropped patch for proxmox-sys, Fabian fixed the issue
    using another approach


proxmox-yew-comp:

Lukas Wagner (3):
  node status panel: add `children` property
  RRDGrid: fix size observer by attaching node reference to rendered
    container
  RRDGrid: add padding and increase gap between elements

 src/node_status_panel.rs | 16 ++++++++++++++++
 src/rrd_grid.rs          |  5 +++--
 2 files changed, 19 insertions(+), 2 deletions(-)


proxmox-datacenter-manager:

Lukas Wagner (8):
  metric collection: clarify naming for remote metric collection
  metric collection: fix minor typo in error message
  metric collection: collect PDM host metrics in a new collection task
  api: fix /nodes/localhost/rrddata endpoint
  pdm: node rrd data: rename 'total-time' to
    'metric-collection-total-time'
  pdm-api-types: add PDM host metric fields
  ui: node status: add RRD graphs for PDM host metrics
  ui: lxc/qemu/node: use RRD value render helpers

 Cargo.toml                                    |   2 +
 cli/client/src/metric_collection.rs           |   4 +-
 debian/control                                |   2 +
 lib/pdm-api-types/src/metric_collection.rs    |   2 +-
 lib/pdm-api-types/src/rrddata.rs              |  74 ++++-
 lib/pdm-client/src/lib.rs                     |   8 +-
 server/Cargo.toml                             |   2 +
 server/src/api/metric_collection.rs           |  10 +-
 server/src/api/nodes/mod.rs                   |   2 +-
 server/src/api/nodes/rrddata.rs               |  73 +++-
 server/src/api/remotes.rs                     |   2 +-
 server/src/api/rrd_common.rs                  |   2 +-
 .../local_collection_task.rs                  | 199 +++++++++++
 server/src/metric_collection/mod.rs           |  40 ++-
 ...tion_task.rs => remote_collection_task.rs} |   8 +-
 server/src/metric_collection/rrd_task.rs      | 187 ++++++++++-
 server/src/metric_collection/state.rs         |   2 +-
 ui/src/administration/node_status.rs          | 312 +++++++++++++++++-
 ui/src/pbs/node/overview.rs                   |  29 +-
 ui/src/pve/lxc/overview.rs                    |  34 +-
 ui/src/pve/node/overview.rs                   |  29 +-
 ui/src/pve/qemu/overview.rs                   |  34 +-
 ui/src/renderer.rs                            |  49 +++
 23 files changed, 955 insertions(+), 151 deletions(-)
 create mode 100644 server/src/metric_collection/local_collection_task.rs
 rename server/src/metric_collection/{collection_task.rs => remote_collection_task.rs} (99%)


Summary over all repositories:
  25 files changed, 974 insertions(+), 153 deletions(-)

-- 
Generated by murpp 0.11.0




^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH proxmox-yew-comp v3 01/11] node status panel: add `children` property
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 02/11] RRDGrid: fix size observer by attaching node reference to rendered container Lukas Wagner
                   ` (9 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

This property allows us to pass child VNodes that should be rendered on
the same panel, allowing us to inject product specific UI elements.

We also implement the ContainerBuilder trait for NodeStatusPanel,
allowing us to use methods such as `with_child`, `with_optional_child`
etc.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 src/node_status_panel.rs | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/src/node_status_panel.rs b/src/node_status_panel.rs
index 357c328..6dca90a 100644
--- a/src/node_status_panel.rs
+++ b/src/node_status_panel.rs
@@ -35,6 +35,10 @@ pub struct NodeStatusPanel {
     #[builder(IntoPropValue, into_prop_value)]
     #[prop_or_default]
     power_management_buttons: bool,
+
+    /// Children that should be rendered on this panel
+    #[prop_or_default]
+    children: Vec<VNode>,
 }
 
 impl NodeStatusPanel {
@@ -49,6 +53,12 @@ impl Default for NodeStatusPanel {
     }
 }
 
+impl ContainerBuilder for NodeStatusPanel {
+    fn as_children_mut(&mut self) -> &mut Vec<VNode> {
+        &mut self.children
+    }
+}
+
 enum Msg {
     Error(Error),
     Loaded(Rc<NodeStatus>),
@@ -203,6 +213,8 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
     }
 
     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
+        let props = ctx.props();
+
         let status = self
             .node_status
             .as_ref()
@@ -222,6 +234,10 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
             .with_child(node_info(status))
             .with_optional_child(self.error.as_ref().map(|e| error_message(&e.to_string())));
 
+        for c in props.children.clone() {
+            panel = panel.with_child(c);
+        }
+
         if ctx.props().power_management_buttons {
             panel.add_tool(
                 ConfirmButton::new(tr!("Reboot"))
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH proxmox-yew-comp v3 02/11] RRDGrid: fix size observer by attaching node reference to rendered container
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
  2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 01/11] node status panel: add `children` property Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 03/11] RRDGrid: add padding and increase gap between elements Lukas Wagner
                   ` (8 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

Without it, the cast to web_sys::Element does not return anything and
the DomSizeObserver is never initialized.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 src/rrd_grid.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/rrd_grid.rs b/src/rrd_grid.rs
index 354f1bf..10f1db7 100644
--- a/src/rrd_grid.rs
+++ b/src/rrd_grid.rs
@@ -77,7 +77,7 @@ impl Component for ProxmoxRRDGrid {
                     .children(props.children.clone()),
             )
             .with_child(html! {<div class="pwt-flex-fill"/>})
-            .into()
+            .into_html_with_ref(self.node_ref.clone())
     }
 
     fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH proxmox-yew-comp v3 03/11] RRDGrid: add padding and increase gap between elements
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
  2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 01/11] node status panel: add `children` property Lukas Wagner
  2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 02/11] RRDGrid: fix size observer by attaching node reference to rendered container Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 04/11] metric collection: clarify naming for remote metric collection Lukas Wagner
                   ` (7 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

This seems visually a tiny bit nicer and avoids the overflow scrollbar
covering child elements.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 src/rrd_grid.rs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/rrd_grid.rs b/src/rrd_grid.rs
index 10f1db7..271401b 100644
--- a/src/rrd_grid.rs
+++ b/src/rrd_grid.rs
@@ -69,7 +69,8 @@ impl Component for ProxmoxRRDGrid {
             .with_child(
                 Container::new()
                     .class(Display::Grid)
-                    .class("pwt-gap-2 pwt-w-100")
+                    .class("pwt-gap-4 pwt-w-100")
+                    .padding(4)
                     .attribute(
                         "style",
                         format!("grid-template-columns:repeat({}, 1fr);", self.cols),
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 04/11] metric collection: clarify naming for remote metric collection
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (2 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 03/11] RRDGrid: add padding and increase gap between elements Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 05/11] metric collection: fix minor typo in error message Lukas Wagner
                   ` (6 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

Primarily to avoid confusion with the new task that will be added for
local metric collection.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 cli/client/src/metric_collection.rs           |  4 ++--
 lib/pdm-api-types/src/metric_collection.rs    |  2 +-
 lib/pdm-client/src/lib.rs                     |  6 +++---
 server/src/api/metric_collection.rs           | 10 +++++-----
 server/src/api/remotes.rs                     |  2 +-
 server/src/api/rrd_common.rs                  |  2 +-
 server/src/metric_collection/mod.rs           | 19 +++++++++++--------
 ...tion_task.rs => remote_collection_task.rs} |  8 ++++----
 server/src/metric_collection/rrd_task.rs      |  2 +-
 server/src/metric_collection/state.rs         |  2 +-
 10 files changed, 30 insertions(+), 27 deletions(-)
 rename server/src/metric_collection/{collection_task.rs => remote_collection_task.rs} (99%)

diff --git a/cli/client/src/metric_collection.rs b/cli/client/src/metric_collection.rs
index e9dbd804..77dcaab5 100644
--- a/cli/client/src/metric_collection.rs
+++ b/cli/client/src/metric_collection.rs
@@ -34,7 +34,7 @@ pub fn cli() -> CommandLineInterface {
 /// all.
 async fn trigger_metric_collection(remote: Option<String>) -> Result<(), Error> {
     client()?
-        .trigger_metric_collection(remote.as_deref())
+        .trigger_remote_metric_collection(remote.as_deref())
         .await?;
     Ok(())
 }
@@ -42,7 +42,7 @@ async fn trigger_metric_collection(remote: Option<String>) -> Result<(), Error>
 #[api]
 /// Show metric collection status.
 async fn metric_collection_status() -> Result<(), Error> {
-    let result = client()?.get_metric_collection_status().await?;
+    let result = client()?.get_remote_metric_collection_status().await?;
 
     let output_format = env().format_args.output_format;
     if output_format == OutputFormat::Text {
diff --git a/lib/pdm-api-types/src/metric_collection.rs b/lib/pdm-api-types/src/metric_collection.rs
index 5279c8a4..cda6ac2a 100644
--- a/lib/pdm-api-types/src/metric_collection.rs
+++ b/lib/pdm-api-types/src/metric_collection.rs
@@ -8,7 +8,7 @@ use proxmox_schema::api;
 #[derive(Clone, Deserialize, Serialize)]
 #[serde(rename_all = "kebab-case")]
 /// Per-remote collection status.
-pub struct MetricCollectionStatus {
+pub struct RemoteMetricCollectionStatus {
     /// The remote's name.
     pub remote: String,
     /// Any error that occured during the last collection attempt.
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 1565869c..8324a27d 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -347,7 +347,7 @@ impl<T: HttpApiClient> PdmClient<T> {
     }
 
     /// Trigger metric collection for a single remote or for all remotes, if no remote is provided.
-    pub async fn trigger_metric_collection(
+    pub async fn trigger_remote_metric_collection(
         &self,
         remote: Option<&str>,
     ) -> Result<(), proxmox_client::Error> {
@@ -368,9 +368,9 @@ impl<T: HttpApiClient> PdmClient<T> {
     }
 
     /// Get global metric collection status.
-    pub async fn get_metric_collection_status(
+    pub async fn get_remote_metric_collection_status(
         &self,
-    ) -> Result<Vec<pdm_api_types::MetricCollectionStatus>, Error> {
+    ) -> Result<Vec<pdm_api_types::RemoteMetricCollectionStatus>, Error> {
         let path = "/api2/extjs/remotes/metric-collection/status";
         Ok(self.0.get(path).await?.expect_json()?.data)
     }
diff --git a/server/src/api/metric_collection.rs b/server/src/api/metric_collection.rs
index b4c81c68..5a480b36 100644
--- a/server/src/api/metric_collection.rs
+++ b/server/src/api/metric_collection.rs
@@ -4,7 +4,7 @@ use proxmox_router::{Router, SubdirMap};
 use proxmox_schema::api;
 use proxmox_sortable_macro::sortable;
 
-use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, MetricCollectionStatus};
+use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteMetricCollectionStatus};
 
 use crate::metric_collection;
 
@@ -34,7 +34,7 @@ const SUBDIRS: SubdirMap = &sorted!([
 )]
 /// Trigger metric collection for a provided remote or for all remotes if no remote is passed.
 pub async fn trigger_metric_collection(remote: Option<String>) -> Result<(), Error> {
-    crate::metric_collection::trigger_metric_collection(remote, false).await?;
+    crate::metric_collection::trigger_remote_metric_collection(remote, false).await?;
 
     Ok(())
 }
@@ -44,11 +44,11 @@ pub async fn trigger_metric_collection(remote: Option<String>) -> Result<(), Err
         type: Array,
         description: "A list of metric collection statuses.",
         items: {
-            type: MetricCollectionStatus,
+            type: RemoteMetricCollectionStatus,
         }
     }
 )]
 /// Read metric collection status.
-fn get_metric_collection_status() -> Result<Vec<MetricCollectionStatus>, Error> {
-    metric_collection::get_status()
+fn get_metric_collection_status() -> Result<Vec<RemoteMetricCollectionStatus>, Error> {
+    metric_collection::remote_metric_collection_status()
 }
diff --git a/server/src/api/remotes.rs b/server/src/api/remotes.rs
index 9700611d..678e0ed2 100644
--- a/server/src/api/remotes.rs
+++ b/server/src/api/remotes.rs
@@ -324,7 +324,7 @@ pub async fn add_remote(mut entry: Remote, create_token: Option<String>) -> Resu
 
     pdm_config::remotes::save_config(remotes)?;
 
-    if let Err(e) = metric_collection::trigger_metric_collection(Some(name), false).await {
+    if let Err(e) = metric_collection::trigger_remote_metric_collection(Some(name), false).await {
         log::error!("could not trigger metric collection after adding remote: {e}");
     }
 
diff --git a/server/src/api/rrd_common.rs b/server/src/api/rrd_common.rs
index b5d1a786..8c0fb798 100644
--- a/server/src/api/rrd_common.rs
+++ b/server/src/api/rrd_common.rs
@@ -74,7 +74,7 @@ pub async fn get_rrd_datapoints<T: DataPoint + Send + 'static>(
         // is super slow or if the metric collection tasks currently busy with collecting other
         // metrics, we just return the data we already have, not the newest one.
         let _ = tokio::time::timeout(WAIT_FOR_NEWEST_METRIC_TIMEOUT, async {
-            metric_collection::trigger_metric_collection(Some(remote), true).await
+            metric_collection::trigger_remote_metric_collection(Some(remote), true).await
         })
         .await;
     }
diff --git a/server/src/metric_collection/mod.rs b/server/src/metric_collection/mod.rs
index 0e6860fc..6bc534f8 100644
--- a/server/src/metric_collection/mod.rs
+++ b/server/src/metric_collection/mod.rs
@@ -7,16 +7,16 @@ use nix::sys::stat::Mode;
 use tokio::sync::mpsc::{self, Sender};
 use tokio::sync::oneshot;
 
-use pdm_api_types::MetricCollectionStatus;
+use pdm_api_types::RemoteMetricCollectionStatus;
 use pdm_buildcfg::PDM_STATE_DIR_M;
 
-mod collection_task;
+mod remote_collection_task;
 pub mod rrd_cache;
 mod rrd_task;
 mod state;
 pub mod top_entities;
 
-use collection_task::{ControlMsg, MetricCollectionTask};
+use remote_collection_task::{ControlMsg, RemoteMetricCollectionTask};
 use rrd_cache::RrdCache;
 
 const RRD_CACHE_BASEDIR: &str = concat!(PDM_STATE_DIR_M!(), "/rrdb");
@@ -46,7 +46,7 @@ pub fn start_task() -> Result<(), Error> {
 
     tokio::spawn(async move {
         let metric_collection_task_future = pin!(async move {
-            match MetricCollectionTask::new(metric_data_tx, trigger_collection_rx) {
+            match RemoteMetricCollectionTask::new(metric_data_tx, trigger_collection_rx) {
                 Ok(mut task) => task.run().await,
                 Err(err) => log::error!("could not start metric collection task: {err}"),
             }
@@ -76,7 +76,10 @@ pub fn start_task() -> Result<(), Error> {
 ///
 /// Has no effect if the tx end of the channel has not been initialized yet.
 /// Returns an error if the mpsc channel has been closed already.
-pub async fn trigger_metric_collection(remote: Option<String>, wait: bool) -> Result<(), Error> {
+pub async fn trigger_remote_metric_collection(
+    remote: Option<String>,
+    wait: bool,
+) -> Result<(), Error> {
     let (done_sender, done_receiver) = oneshot::channel();
 
     if let Some(sender) = CONTROL_MESSAGE_TX.get() {
@@ -93,15 +96,15 @@ pub async fn trigger_metric_collection(remote: Option<String>, wait: bool) -> Re
 }
 
 /// Get each remote's metric collection status.
-pub fn get_status() -> Result<Vec<MetricCollectionStatus>, Error> {
+pub fn remote_metric_collection_status() -> Result<Vec<RemoteMetricCollectionStatus>, Error> {
     let (remotes, _) = pdm_config::remotes::config()?;
-    let state = collection_task::load_state()?;
+    let state = remote_collection_task::load_state()?;
 
     let mut result = Vec::new();
 
     for (remote, _) in remotes.into_iter() {
         if let Some(status) = state.get_status(&remote) {
-            result.push(MetricCollectionStatus {
+            result.push(RemoteMetricCollectionStatus {
                 remote,
                 error: status.error.clone(),
                 last_collection: status.last_collection,
diff --git a/server/src/metric_collection/collection_task.rs b/server/src/metric_collection/remote_collection_task.rs
similarity index 99%
rename from server/src/metric_collection/collection_task.rs
rename to server/src/metric_collection/remote_collection_task.rs
index cc1a460e..eca0e11d 100644
--- a/server/src/metric_collection/collection_task.rs
+++ b/server/src/metric_collection/remote_collection_task.rs
@@ -46,13 +46,13 @@ pub(super) enum ControlMsg {
 
 /// Task which periodically collects metrics from all remotes and stores
 /// them in the local metrics database.
-pub(super) struct MetricCollectionTask {
+pub(super) struct RemoteMetricCollectionTask {
     state: MetricCollectionState,
     metric_data_tx: Sender<RrdStoreRequest>,
     control_message_rx: Receiver<ControlMsg>,
 }
 
-impl MetricCollectionTask {
+impl RemoteMetricCollectionTask {
     /// Create a new metric collection task.
     pub(super) fn new(
         metric_data_tx: Sender<RrdStoreRequest>,
@@ -574,7 +574,7 @@ pub(super) mod tests {
 
         let (_control_tx, control_rx) = tokio::sync::mpsc::channel(10);
 
-        let mut task = MetricCollectionTask {
+        let mut task = RemoteMetricCollectionTask {
             state,
             metric_data_tx: tx,
             control_message_rx: control_rx,
@@ -644,7 +644,7 @@ pub(super) mod tests {
 
         let (_control_tx, control_rx) = tokio::sync::mpsc::channel(10);
 
-        let mut task = MetricCollectionTask {
+        let mut task = RemoteMetricCollectionTask {
             state,
             metric_data_tx: tx,
             control_message_rx: control_rx,
diff --git a/server/src/metric_collection/rrd_task.rs b/server/src/metric_collection/rrd_task.rs
index 48b6de9e..29137858 100644
--- a/server/src/metric_collection/rrd_task.rs
+++ b/server/src/metric_collection/rrd_task.rs
@@ -200,7 +200,7 @@ mod tests {
     use pve_api_types::{ClusterMetrics, ClusterMetricsData};
 
     use crate::{
-        metric_collection::collection_task::tests::get_create_options,
+        metric_collection::remote_collection_task::tests::get_create_options,
         test_support::temp::NamedTempDir,
     };
 
diff --git a/server/src/metric_collection/state.rs b/server/src/metric_collection/state.rs
index 7f68843e..fd313c48 100644
--- a/server/src/metric_collection/state.rs
+++ b/server/src/metric_collection/state.rs
@@ -92,7 +92,7 @@ impl MetricCollectionState {
 
 #[cfg(test)]
 mod tests {
-    use crate::metric_collection::collection_task::tests::get_create_options;
+    use crate::metric_collection::remote_collection_task::tests::get_create_options;
     use crate::test_support::temp::NamedTempFile;
 
     use super::*;
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 05/11] metric collection: fix minor typo in error message
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (3 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 04/11] metric collection: clarify naming for remote metric collection Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 06/11] metric collection: collect PDM host metrics in a new collection task Lukas Wagner
                   ` (5 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 server/src/metric_collection/mod.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/src/metric_collection/mod.rs b/server/src/metric_collection/mod.rs
index 6bc534f8..3cd58148 100644
--- a/server/src/metric_collection/mod.rs
+++ b/server/src/metric_collection/mod.rs
@@ -41,7 +41,7 @@ pub fn start_task() -> Result<(), Error> {
 
     let (trigger_collection_tx, trigger_collection_rx) = mpsc::channel(128);
     if CONTROL_MESSAGE_TX.set(trigger_collection_tx).is_err() {
-        bail!("control message sender alread set");
+        bail!("control message sender already set");
     }
 
     tokio::spawn(async move {
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 06/11] metric collection: collect PDM host metrics in a new collection task
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (4 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 05/11] metric collection: fix minor typo in error message Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 07/11] api: fix /nodes/localhost/rrddata endpoint Lukas Wagner
                   ` (4 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

Then whole architecture is pretty similar to the remote metric
collection. We introduce a task that fetches host metrics and sends them
via a channel to the RRD task, which is responsible for persisting them
in the RRD database.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 Cargo.toml                                    |   2 +
 debian/control                                |   2 +
 server/Cargo.toml                             |   2 +
 .../local_collection_task.rs                  | 199 ++++++++++++++++++
 server/src/metric_collection/mod.rs           |  21 +-
 server/src/metric_collection/rrd_task.rs      | 185 ++++++++++++++++
 6 files changed, 406 insertions(+), 5 deletions(-)
 create mode 100644 server/src/metric_collection/local_collection_task.rs

diff --git a/Cargo.toml b/Cargo.toml
index ec2aa3dc..b708ee98 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,6 +39,7 @@ proxmox-auth-api = "1.0.5"
 proxmox-base64 = "1"
 proxmox-client = "1"
 proxmox-daemon = "1"
+proxmox-disks = "0.2"
 proxmox-docgen = "1"
 proxmox-http = { version = "1.0.4", features = [ "client", "http-helpers", "websocket" ] } # see below
 proxmox-human-byte = "1"
@@ -47,6 +48,7 @@ proxmox-ldap = { version = "1.1", features = ["sync"] }
 proxmox-lang = "1.1"
 proxmox-log = "1"
 proxmox-login = "1.0.2"
+proxmox-procfs = "0.1"
 proxmox-rest-server = "1"
 # some use "cli", some use "cli" and "server", pbs-config uses nothing
 proxmox-router = { version = "3.0.0", default-features = false }
diff --git a/debian/control b/debian/control
index 4ddc9efc..9101e8cd 100644
--- a/debian/control
+++ b/debian/control
@@ -52,6 +52,7 @@ Build-Depends: debhelper-compat (= 13),
                librust-proxmox-config-digest-1+default-dev,
                librust-proxmox-config-digest-1+openssl-dev,
                librust-proxmox-daemon-1+default-dev,
+               librust-proxmox-disks-0.1+default-dev,
                librust-proxmox-dns-api-1+default-dev,
                librust-proxmox-dns-api-1+impl-dev,
                librust-proxmox-docgen-1+default-dev,
@@ -72,6 +73,7 @@ Build-Depends: debhelper-compat (= 13),
                librust-proxmox-network-api-1+impl-dev,
                librust-proxmox-node-status-1+api-dev,
                librust-proxmox-openid-1+default-dev (>= 1.0.2-~~),
+               librust-proxmox-procfs-0.1+default-dev,
                librust-proxmox-product-config-1+default-dev,
                librust-proxmox-rest-server-1+default-dev,
                librust-proxmox-rest-server-1+templates-dev,
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6969549f..65170864 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-async.workspace = true
 proxmox-auth-api = { workspace = true, features = [ "api", "ticket", "pam-authenticator", "password-authenticator" ] }
 proxmox-base64.workspace = true
 proxmox-daemon.workspace = true
+proxmox-disks.workspace = true
 proxmox-docgen.workspace = true
 proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
 proxmox-lang.workspace = true
@@ -47,6 +48,7 @@ proxmox-ldap.workspace = true
 proxmox-log.workspace = true
 proxmox-login.workspace = true
 proxmox-openid.workspace = true
+proxmox-procfs.workspace = true
 proxmox-rest-server = { workspace = true, features = [ "templates" ] }
 proxmox-router = { workspace = true, features = [ "cli", "server"] }
 proxmox-rrd.workspace = true
diff --git a/server/src/metric_collection/local_collection_task.rs b/server/src/metric_collection/local_collection_task.rs
new file mode 100644
index 00000000..034b51a3
--- /dev/null
+++ b/server/src/metric_collection/local_collection_task.rs
@@ -0,0 +1,199 @@
+use std::sync::Mutex;
+use std::time::Instant;
+use std::{collections::HashMap, time::Duration};
+
+use anyhow::{Context, Error};
+use tokio::{sync::mpsc::Sender, time::MissedTickBehavior};
+
+use proxmox_disks::Disks;
+use proxmox_log::{debug, error};
+use proxmox_network_api::IpLink;
+use proxmox_procfs::pressure::{PressureData, Resource};
+use proxmox_sys::fs;
+use proxmox_sys::linux::procfs;
+
+use super::rrd_task::RrdStoreRequest;
+
+const HOST_METRIC_COLLECTION_INTERVAL: Duration = Duration::from_secs(10);
+
+/// Task which periodically collects metrics from the PDM host and stores
+/// them in the local metrics database.
+pub(super) struct LocalMetricCollectionTask {
+    metric_data_tx: Sender<RrdStoreRequest>,
+}
+
+impl LocalMetricCollectionTask {
+    /// Create a new metric collection task.
+    pub(super) fn new(metric_data_tx: Sender<RrdStoreRequest>) -> Self {
+        Self { metric_data_tx }
+    }
+
+    /// Run the metric collection task.
+    ///
+    /// This function never returns.
+    pub(super) async fn run(&mut self) {
+        let mut timer = tokio::time::interval(HOST_METRIC_COLLECTION_INTERVAL);
+        timer.set_missed_tick_behavior(MissedTickBehavior::Skip);
+
+        loop {
+            timer.tick().await;
+            self.handle_tick().await;
+        }
+    }
+
+    /// Handle a timer tick.
+    async fn handle_tick(&mut self) {
+        let stats = match tokio::task::spawn_blocking(collect_host_metrics).await {
+            Ok(stats) => stats,
+            Err(err) => {
+                error!("join error while collecting host stats: {err}");
+                return;
+            }
+        };
+
+        let _ = self
+            .metric_data_tx
+            .send(RrdStoreRequest::Host {
+                timestamp: proxmox_time::epoch_i64(),
+                metrics: Box::new(stats),
+            })
+            .await;
+    }
+}
+
+/// Container type for various metrics of a PDM host.
+pub(super) struct PdmHostMetrics {
+    /// CPU statistics from `/proc/stat`.
+    pub proc: Option<procfs::ProcFsStat>,
+    /// Memory statistics from `/proc/meminfo`.
+    pub meminfo: Option<procfs::ProcFsMemInfo>,
+    /// System load stats from `/proc/loadavg`.
+    pub load: Option<procfs::Loadavg>,
+    /// Aggregated network device traffic for all physical NICs.
+    pub netstats: Option<NetDevStats>,
+    /// Block device stats for the root disk.
+    pub root_blockdev_stat: Option<proxmox_disks::BlockDevStat>,
+    /// File system usage for the root disk.
+    pub root_filesystem_info: Option<fs::FileSystemInformation>,
+    /// CPU pressure stall information for the host.
+    pub cpu_pressure: Option<PressureData>,
+    /// CPU pressure stall information for the host.
+    pub memory_pressure: Option<PressureData>,
+    /// IO pressure stall information for the host.
+    pub io_pressure: Option<PressureData>,
+}
+
+/// Aggregated network device traffic for all physical NICs.
+pub(super) struct NetDevStats {
+    /// Aggregate inbound traffic over all physical NICs in bytes.
+    pub netin: u64,
+    /// Aggregate outbound traffic over all physical NICs in bytes.
+    pub netout: u64,
+}
+
+fn collect_host_metrics() -> PdmHostMetrics {
+    let proc = procfs::read_proc_stat()
+        .inspect_err(|err| error!("failed to read '/proc/stat': {err:#}"))
+        .ok();
+
+    let meminfo = procfs::read_meminfo()
+        .inspect_err(|err| error!("failed to read '/proc/meminfo': {err:#}"))
+        .ok();
+
+    let cpu_pressure = PressureData::read_system(Resource::Cpu)
+        .inspect_err(|err| error!("failed to read CPU pressure stall information: {err:#}"))
+        .ok();
+
+    let memory_pressure = PressureData::read_system(Resource::Memory)
+        .inspect_err(|err| error!("failed to read memory pressure stall information: {err:#}"))
+        .ok();
+
+    let io_pressure = PressureData::read_system(Resource::Io)
+        .inspect_err(|err| error!("failed to read IO pressure stall information: {err:#}"))
+        .ok();
+
+    let load = procfs::read_loadavg()
+        .inspect_err(|err| error!("failed to read '/proc/loadavg': {err:#}"))
+        .ok();
+
+    let root_blockdev_stat = Disks::new()
+        .blockdev_stat_for_path("/")
+        .inspect_err(|err| error!("failed to collect blockdev statistics for '/': {err:#}"))
+        .ok();
+
+    let root_filesystem_info = proxmox_sys::fs::fs_info("/")
+        .inspect_err(|err| {
+            error!("failed to query filesystem usage for '/': {err:#}");
+        })
+        .ok();
+
+    let netstats = collect_netdev_metrics()
+        .inspect_err(|err| {
+            error!("failed to collect network device statistics: {err:#}");
+        })
+        .ok();
+
+    PdmHostMetrics {
+        proc,
+        meminfo,
+        load,
+        netstats,
+        root_blockdev_stat,
+        root_filesystem_info,
+        cpu_pressure,
+        memory_pressure,
+        io_pressure,
+    }
+}
+
+struct NetdevCacheEntry {
+    interfaces: HashMap<String, IpLink>,
+    timestamp: Instant,
+}
+
+const NETWORK_INTERFACE_CACHE_MAX_AGE: Duration = Duration::from_secs(300);
+static NETWORK_INTERFACE_CACHE: Mutex<Option<NetdevCacheEntry>> = Mutex::new(None);
+
+fn collect_netdev_metrics() -> Result<NetDevStats, Error> {
+    let net_devs = procfs::read_proc_net_dev()?;
+
+    let mut cache = NETWORK_INTERFACE_CACHE.lock().unwrap();
+
+    let now = Instant::now();
+
+    let needs_refresh = match cache.as_ref() {
+        Some(entry) => now.duration_since(entry.timestamp) > NETWORK_INTERFACE_CACHE_MAX_AGE,
+        None => true,
+    };
+
+    if needs_refresh {
+        cache.replace({
+            debug!("updating cached network devices");
+
+            let interfaces = proxmox_network_api::get_network_interfaces()
+                .context("failed to enumerate network devices")?;
+
+            NetdevCacheEntry {
+                interfaces,
+                timestamp: now,
+            }
+        });
+    }
+
+    // unwrap: at this point we *know* that the Option is Some
+    let ip_links = cache.as_ref().unwrap();
+
+    let mut netin = 0;
+    let mut netout = 0;
+
+    for net_dev in net_devs {
+        if let Some(ip_link) = ip_links.interfaces.get(&net_dev.device) {
+            if ip_link.is_physical() {
+                netin += net_dev.receive;
+                netout += net_dev.send;
+            }
+        }
+    }
+
+    Ok(NetDevStats { netin, netout })
+}
diff --git a/server/src/metric_collection/mod.rs b/server/src/metric_collection/mod.rs
index 3cd58148..8a945fab 100644
--- a/server/src/metric_collection/mod.rs
+++ b/server/src/metric_collection/mod.rs
@@ -10,6 +10,7 @@ use tokio::sync::oneshot;
 use pdm_api_types::RemoteMetricCollectionStatus;
 use pdm_buildcfg::PDM_STATE_DIR_M;
 
+mod local_collection_task;
 mod remote_collection_task;
 pub mod rrd_cache;
 mod rrd_task;
@@ -19,6 +20,8 @@ pub mod top_entities;
 use remote_collection_task::{ControlMsg, RemoteMetricCollectionTask};
 use rrd_cache::RrdCache;
 
+use crate::metric_collection::local_collection_task::LocalMetricCollectionTask;
+
 const RRD_CACHE_BASEDIR: &str = concat!(PDM_STATE_DIR_M!(), "/rrdb");
 
 static CONTROL_MESSAGE_TX: OnceLock<Sender<ControlMsg>> = OnceLock::new();
@@ -39,14 +42,22 @@ pub fn init() -> Result<(), Error> {
 pub fn start_task() -> Result<(), Error> {
     let (metric_data_tx, metric_data_rx) = mpsc::channel(128);
 
+    let cache = rrd_cache::get_cache();
+    tokio::spawn(async move {
+        let rrd_task_future = pin!(rrd_task::store_in_rrd_task(cache, metric_data_rx));
+        let abort_future = pin!(proxmox_daemon::shutdown_future());
+        futures::future::select(rrd_task_future, abort_future).await;
+    });
+
     let (trigger_collection_tx, trigger_collection_rx) = mpsc::channel(128);
     if CONTROL_MESSAGE_TX.set(trigger_collection_tx).is_err() {
         bail!("control message sender already set");
     }
 
+    let metric_data_tx_clone = metric_data_tx.clone();
     tokio::spawn(async move {
         let metric_collection_task_future = pin!(async move {
-            match RemoteMetricCollectionTask::new(metric_data_tx, trigger_collection_rx) {
+            match RemoteMetricCollectionTask::new(metric_data_tx_clone, trigger_collection_rx) {
                 Ok(mut task) => task.run().await,
                 Err(err) => log::error!("could not start metric collection task: {err}"),
             }
@@ -56,12 +67,12 @@ pub fn start_task() -> Result<(), Error> {
         futures::future::select(metric_collection_task_future, abort_future).await;
     });
 
-    let cache = rrd_cache::get_cache();
-
     tokio::spawn(async move {
-        let rrd_task_future = pin!(rrd_task::store_in_rrd_task(cache, metric_data_rx));
+        let metric_collection_task_future =
+            pin!(async move { LocalMetricCollectionTask::new(metric_data_tx).run().await });
+
         let abort_future = pin!(proxmox_daemon::shutdown_future());
-        futures::future::select(rrd_task_future, abort_future).await;
+        futures::future::select(metric_collection_task_future, abort_future).await;
     });
 
     Ok(())
diff --git a/server/src/metric_collection/rrd_task.rs b/server/src/metric_collection/rrd_task.rs
index 29137858..4cf18679 100644
--- a/server/src/metric_collection/rrd_task.rs
+++ b/server/src/metric_collection/rrd_task.rs
@@ -8,6 +8,7 @@ use proxmox_rrd::rrd::DataSourceType;
 use pbs_api_types::{MetricDataPoint, MetricDataType, Metrics};
 use pve_api_types::{ClusterMetrics, ClusterMetricsData, ClusterMetricsDataType};
 
+use super::local_collection_task::PdmHostMetrics;
 use super::rrd_cache::RrdCache;
 
 /// Store request for the RRD task.
@@ -45,6 +46,16 @@ pub(super) enum RrdStoreRequest {
         /// Statistics.
         stats: CollectionStats,
     },
+    /// Store PDM host metrics.
+    Host {
+        /// Timestamp at which the metrics were collected (UNIX epoch).
+        timestamp: i64,
+
+        /// Metric data for this PDM host.
+        // Boxed to avoid a clippy warning regarding large size differences between
+        // enum variants.
+        metrics: Box<PdmHostMetrics>,
+    },
 }
 
 /// Result for a [`RrdStoreRequest`].
@@ -117,6 +128,9 @@ pub(super) async fn store_in_rrd_task(
                 RrdStoreRequest::CollectionStats { timestamp, stats } => {
                     store_stats(&cache_clone, &stats, timestamp)
                 }
+                RrdStoreRequest::Host { timestamp, metrics } => {
+                    store_pdm_host_metrics(&cache_clone, timestamp, &metrics)
+                }
             };
         })
         .await;
@@ -194,6 +208,177 @@ fn store_stats(cache: &RrdCache, stats: &CollectionStats, timestamp: i64) {
     );
 }
 
+fn store_pdm_host_metrics(cache: &RrdCache, timestamp: i64, metrics: &PdmHostMetrics) {
+    if let Some(proc) = &metrics.proc {
+        cache.update_value(
+            "nodes/localhost/cpu-current",
+            proc.cpu,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/cpu-iowait",
+            proc.iowait_percent,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+    }
+
+    if let Some(load) = &metrics.load {
+        cache.update_value(
+            "nodes/localhost/cpu-avg1",
+            load.0,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/cpu-avg5",
+            load.1,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/cpu-avg15",
+            load.2,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+    }
+
+    if let Some(cpu_pressure) = &metrics.cpu_pressure {
+        cache.update_value(
+            "nodes/localhost/cpu-pressure-some-avg10",
+            cpu_pressure.some.average_10,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+
+        // NOTE: On a system level, 'full' CPU pressure is undefined and reported as 0,
+        // so it does not make sense to store it.
+        // https://docs.kernel.org/accounting/psi.html#pressure-interface
+    }
+
+    if let Some(meminfo) = &metrics.meminfo {
+        cache.update_value(
+            "nodes/localhost/mem-total",
+            meminfo.memtotal as f64,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/mem-used",
+            meminfo.memused as f64,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/swap-total",
+            meminfo.swaptotal as f64,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/swap-used",
+            meminfo.swapused as f64,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+    }
+
+    if let Some(memory_pressure) = &metrics.memory_pressure {
+        cache.update_value(
+            "nodes/localhost/mem-pressure-some-avg10",
+            memory_pressure.some.average_10,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/mem-pressure-full-avg10",
+            memory_pressure.full.average_10,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+    }
+
+    if let Some(netstats) = &metrics.netstats {
+        cache.update_value(
+            "nodes/localhost/net-in",
+            netstats.netin as f64,
+            timestamp,
+            DataSourceType::Derive,
+        );
+        cache.update_value(
+            "nodes/localhost/net-out",
+            netstats.netout as f64,
+            timestamp,
+            DataSourceType::Derive,
+        );
+    }
+
+    if let Some(disk) = &metrics.root_filesystem_info {
+        cache.update_value(
+            "nodes/localhost/disk-total",
+            disk.total as f64,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/disk-used",
+            disk.used as f64,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+    }
+
+    if let Some(stat) = &metrics.root_blockdev_stat {
+        cache.update_value(
+            "nodes/localhost/disk-read-iops",
+            stat.read_ios as f64,
+            timestamp,
+            DataSourceType::Derive,
+        );
+        cache.update_value(
+            "nodes/localhost/disk-write-iops",
+            stat.write_ios as f64,
+            timestamp,
+            DataSourceType::Derive,
+        );
+        cache.update_value(
+            "nodes/localhost/disk-read",
+            (stat.read_sectors * 512) as f64,
+            timestamp,
+            DataSourceType::Derive,
+        );
+        cache.update_value(
+            "nodes/localhost/disk-write",
+            (stat.write_sectors * 512) as f64,
+            timestamp,
+            DataSourceType::Derive,
+        );
+        cache.update_value(
+            "nodes/localhost/disk-io-ticks",
+            (stat.io_ticks as f64) / 1000.0,
+            timestamp,
+            DataSourceType::Derive,
+        );
+    }
+
+    if let Some(io_pressure) = &metrics.io_pressure {
+        cache.update_value(
+            "nodes/localhost/io-pressure-some-avg10",
+            io_pressure.some.average_10,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+        cache.update_value(
+            "nodes/localhost/io-pressure-full-avg10",
+            io_pressure.full.average_10,
+            timestamp,
+            DataSourceType::Gauge,
+        );
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use proxmox_rrd_api_types::{RrdMode, RrdTimeframe};
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 07/11] api: fix /nodes/localhost/rrddata endpoint
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (5 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 06/11] metric collection: collect PDM host metrics in a new collection task Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 08/11] pdm: node rrd data: rename 'total-time' to 'metric-collection-total-time' Lukas Wagner
                   ` (3 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

We didn't use this existing endpoint so far, which is why this mistake
was not discovered yet. First, there was a typo in the API handler path,
and second the `node` parameter was missing from the handler itself.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 lib/pdm-client/src/lib.rs       |  2 +-
 server/src/api/nodes/mod.rs     |  2 +-
 server/src/api/nodes/rrddata.rs | 18 ++++++++++++++++--
 3 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 8324a27d..00f1b3f9 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -380,7 +380,7 @@ impl<T: HttpApiClient> PdmClient<T> {
         &self,
         mode: RrdMode,
         timeframe: RrdTimeframe,
-    ) -> Result<pdm_api_types::rrddata::PdmNodeDatapoint, Error> {
+    ) -> Result<Vec<pdm_api_types::rrddata::PdmNodeDatapoint>, Error> {
         let path = ApiPathBuilder::new("/api2/extjs/nodes/localhost/rrddata")
             .arg("cf", mode)
             .arg("timeframe", timeframe)
diff --git a/server/src/api/nodes/mod.rs b/server/src/api/nodes/mod.rs
index bd1396bc..7903d63a 100644
--- a/server/src/api/nodes/mod.rs
+++ b/server/src/api/nodes/mod.rs
@@ -48,7 +48,7 @@ pub const SUBDIRS: SubdirMap = &sorted!([
     ("journal", &journal::ROUTER),
     ("network", &network::ROUTER),
     ("report", &report::ROUTER),
-    ("rrdata", &rrddata::ROUTER),
+    ("rrddata", &rrddata::ROUTER),
     ("sdn", &sdn::ROUTER),
     ("subscription", &subscription::ROUTER),
     ("status", &status::ROUTER),
diff --git a/server/src/api/nodes/rrddata.rs b/server/src/api/nodes/rrddata.rs
index 75900965..4c2302c8 100644
--- a/server/src/api/nodes/rrddata.rs
+++ b/server/src/api/nodes/rrddata.rs
@@ -1,10 +1,11 @@
 use anyhow::Error;
 use proxmox_rrd_api_types::{RrdMode, RrdTimeframe};
 
-use proxmox_router::Router;
+use proxmox_router::{http_bail, Router};
 use proxmox_schema::api;
 
 use pdm_api_types::rrddata::PdmNodeDatapoint;
+use pdm_api_types::NODE_SCHEMA;
 
 use crate::api::rrd_common::{self, DataPoint};
 
@@ -36,6 +37,9 @@ impl DataPoint for PdmNodeDatapoint {
             cf: {
                 type: RrdMode,
             },
+            node: {
+                schema: NODE_SCHEMA,
+            },
         },
     },
     returns: {
@@ -47,7 +51,17 @@ impl DataPoint for PdmNodeDatapoint {
     }
 )]
 /// Read RRD data for this PDM node.
-fn get_node_rrddata(timeframe: RrdTimeframe, cf: RrdMode) -> Result<Vec<PdmNodeDatapoint>, Error> {
+fn get_node_rrddata(
+    node: String,
+    timeframe: RrdTimeframe,
+    cf: RrdMode,
+) -> Result<Vec<PdmNodeDatapoint>, Error> {
+    if node != "localhost" {
+        http_bail!(
+            BAD_REQUEST,
+            "PDM only supports `localhost` as a `node` parameter"
+        );
+    }
     let base = "nodes/localhost";
     rrd_common::create_datapoints_from_rrd(base, timeframe, cf)
 }
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 08/11] pdm: node rrd data: rename 'total-time' to 'metric-collection-total-time'
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (6 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 07/11] api: fix /nodes/localhost/rrddata endpoint Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 09/11] pdm-api-types: add PDM host metric fields Lukas Wagner
                   ` (2 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

In the initial version of the remote metric collection series, there was
a separate API endpoint for metric collection rrd data, hence the short
name. Unfortunately we forgot to rename the field when the metric
collection stats were move the PDM host stats.

Neither the client tool nor the UI used this field yet, and also we
didn't stabilize PDMs API yet, so it should be fine to just rename the
field.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 lib/pdm-api-types/src/rrddata.rs | 2 +-
 server/src/api/nodes/rrddata.rs  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/pdm-api-types/src/rrddata.rs b/lib/pdm-api-types/src/rrddata.rs
index 70619233..6eaaff3c 100644
--- a/lib/pdm-api-types/src/rrddata.rs
+++ b/lib/pdm-api-types/src/rrddata.rs
@@ -242,7 +242,7 @@ pub struct PdmNodeDatapoint {
 
     /// Total time in milliseconds needed for full metric collection run.
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub total_time: Option<f64>,
+    pub metric_collection_total_time: Option<f64>,
 }
 
 #[api]
diff --git a/server/src/api/nodes/rrddata.rs b/server/src/api/nodes/rrddata.rs
index 4c2302c8..00c4eee0 100644
--- a/server/src/api/nodes/rrddata.rs
+++ b/server/src/api/nodes/rrddata.rs
@@ -23,7 +23,7 @@ impl DataPoint for PdmNodeDatapoint {
 
     fn set_field(&mut self, name: &str, value: f64) {
         if name == "metric-collection-total-time" {
-            self.total_time = Some(value);
+            self.metric_collection_total_time = Some(value);
         }
     }
 }
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 09/11] pdm-api-types: add PDM host metric fields
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (7 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 08/11] pdm: node rrd data: rename 'total-time' to 'metric-collection-total-time' Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 10/11] ui: node status: add RRD graphs for PDM host metrics Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 11/11] ui: lxc/qemu/node: use RRD value render helpers Lukas Wagner
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 lib/pdm-api-types/src/rrddata.rs | 72 +++++++++++++++++++++++++++++++-
 server/src/api/nodes/rrddata.rs  | 55 ++++++++++++++++++++++--
 2 files changed, 122 insertions(+), 5 deletions(-)

diff --git a/lib/pdm-api-types/src/rrddata.rs b/lib/pdm-api-types/src/rrddata.rs
index 6eaaff3c..452597a8 100644
--- a/lib/pdm-api-types/src/rrddata.rs
+++ b/lib/pdm-api-types/src/rrddata.rs
@@ -233,13 +233,81 @@ pub struct PbsDatastoreDataPoint {
 }
 
 #[api]
-#[derive(Serialize, Deserialize, Default)]
+#[derive(Serialize, Deserialize, Default, Debug)]
 #[serde(rename_all = "kebab-case")]
 /// RRD datapoint for statistics about the metric collection loop.
 pub struct PdmNodeDatapoint {
     /// Timestamp (UNIX epoch)
     pub time: u64,
-
+    /// Current CPU utilization
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpu_current: Option<f64>,
+    /// Current IO wait
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpu_iowait: Option<f64>,
+    /// CPU utilization, averaged over the last minute
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpu_avg1: Option<f64>,
+    /// CPU utilization, averaged over the last five minutes
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpu_avg5: Option<f64>,
+    /// CPU utilization, averaged over the last fifteen minutes
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpu_avg15: Option<f64>,
+    /// Total root disk size
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disk_total: Option<f64>,
+    /// Total root disk usage
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disk_used: Option<f64>,
+    /// Root disk read IOPS
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disk_read_iops: Option<f64>,
+    /// Root disk write IOPS
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disk_write_iops: Option<f64>,
+    /// Root disk read rate
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disk_read: Option<f64>,
+    /// Root disk write rate
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disk_write: Option<f64>,
+    /// Root disk IO ticks
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub disk_io_ticks: Option<f64>,
+    /// Total memory size
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mem_total: Option<f64>,
+    /// Currently used memory
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mem_used: Option<f64>,
+    /// Total swap size
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub swap_total: Option<f64>,
+    /// Current swap usage
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub swap_used: Option<f64>,
+    /// Inbound network data rate
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub net_in: Option<f64>,
+    /// Outbound network data rate
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub net_out: Option<f64>,
+    /// Average 'some' CPU pressure over the last 10 minutes.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpu_pressure_some_avg10: Option<f64>,
+    /// Average 'some' memory pressure over the last 10 minutes.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mem_pressure_some_avg10: Option<f64>,
+    /// Average 'full' memory pressure over the last 10 minutes.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mem_pressure_full_avg10: Option<f64>,
+    /// Average 'some' IO pressure over the last 10 minutes.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub io_pressure_some_avg10: Option<f64>,
+    /// Average 'full' IO pressure over the last 10 minutes.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub io_pressure_full_avg10: Option<f64>,
     /// Total time in milliseconds needed for full metric collection run.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub metric_collection_total_time: Option<f64>,
diff --git a/server/src/api/nodes/rrddata.rs b/server/src/api/nodes/rrddata.rs
index 00c4eee0..8ba11a5f 100644
--- a/server/src/api/nodes/rrddata.rs
+++ b/server/src/api/nodes/rrddata.rs
@@ -18,12 +18,61 @@ impl DataPoint for PdmNodeDatapoint {
     }
 
     fn fields() -> &'static [&'static str] {
-        &["metric-collection-total-time"]
+        &[
+            "cpu-current",
+            "cpu-iowait",
+            "cpu-avg1",
+            "cpu-avg5",
+            "cpu-avg15",
+            "cpu-pressure-some-avg10",
+            "disk-total",
+            "disk-used",
+            "disk-read-iops",
+            "disk-write-iops",
+            "disk-read",
+            "disk-write",
+            "disk-io-ticks",
+            "io-pressure-some-avg10",
+            "io-pressure-full-avg10",
+            "mem-total",
+            "mem-used",
+            "mem-pressure-some-avg10",
+            "mem-pressure-full-avg10",
+            "swap-total",
+            "swap-used",
+            "net-in",
+            "net-out",
+            "metric-collection-total-time",
+        ]
     }
 
     fn set_field(&mut self, name: &str, value: f64) {
-        if name == "metric-collection-total-time" {
-            self.metric_collection_total_time = Some(value);
+        match name {
+            "cpu-current" => self.cpu_current = Some(value),
+            "cpu-iowait" => self.cpu_iowait = Some(value),
+            "cpu-avg1" => self.cpu_avg1 = Some(value),
+            "cpu-avg5" => self.cpu_avg5 = Some(value),
+            "cpu-avg15" => self.cpu_avg15 = Some(value),
+            "cpu-pressure-some-avg10" => self.cpu_pressure_some_avg10 = Some(value),
+            "disk-total" => self.disk_total = Some(value),
+            "disk-used" => self.disk_used = Some(value),
+            "disk-read-iops" => self.disk_read_iops = Some(value),
+            "disk-write-iops" => self.disk_write_iops = Some(value),
+            "disk-read" => self.disk_read = Some(value),
+            "disk-write" => self.disk_write = Some(value),
+            "disk-io-ticks" => self.disk_io_ticks = Some(value),
+            "io-pressure-some-avg10" => self.io_pressure_some_avg10 = Some(value),
+            "io-pressure-full-avg10" => self.io_pressure_full_avg10 = Some(value),
+            "mem-total" => self.mem_total = Some(value),
+            "mem-used" => self.mem_used = Some(value),
+            "mem-pressure-some-avg10" => self.mem_pressure_some_avg10 = Some(value),
+            "mem-pressure-full-avg10" => self.mem_pressure_full_avg10 = Some(value),
+            "swap-total" => self.swap_total = Some(value),
+            "swap-used" => self.swap_used = Some(value),
+            "net-in" => self.net_in = Some(value),
+            "net-out" => self.net_out = Some(value),
+            "metric-collection-total-time" => self.metric_collection_total_time = Some(value),
+            _ => log::error!("setting invalid field '{name}' in PdmNodeDatapoint"),
         }
     }
 }
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 10/11] ui: node status: add RRD graphs for PDM host metrics
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (8 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 09/11] pdm-api-types: add PDM host metric fields Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 11/11] ui: lxc/qemu/node: use RRD value render helpers Lukas Wagner
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

This adds RRD graphs in the existing node status panel. We add graphs
for
  - CPU/IOWait
  - Load-Avg
  - Memory usage
  - Network utilization
  - Pressure (CPU, memory, IO)

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 ui/src/administration/node_status.rs | 312 ++++++++++++++++++++++++++-
 ui/src/renderer.rs                   |  49 +++++
 2 files changed, 354 insertions(+), 7 deletions(-)

diff --git a/ui/src/administration/node_status.rs b/ui/src/administration/node_status.rs
index a61f25a2..62684338 100644
--- a/ui/src/administration/node_status.rs
+++ b/ui/src/administration/node_status.rs
@@ -7,17 +7,28 @@ use proxmox_node_status::NodePowerCommand;
 use proxmox_time::epoch_i64;
 use proxmox_yew_comp::percent_encoding::percent_encode_component;
 use proxmox_yew_comp::utils::{copy_text_to_clipboard, render_epoch};
-use proxmox_yew_comp::{http_post, ConfirmButton, NodeStatusPanel};
-use pwt::prelude::*;
+use proxmox_yew_comp::{
+    http_post, ConfirmButton, NodeStatusPanel, RRDGraph, RRDGrid, RRDTimeframe,
+    RRDTimeframeSelector, Series,
+};
+use pwt::css::JustifyContent;
 use pwt::widget::{Button, Column, Container, Row};
 use pwt::AsyncAbortGuard;
+use pwt::{prelude::*, AsyncPool};
 
-use crate::get_nodename;
+use pdm_api_types::rrddata::PdmNodeDatapoint;
+
+use crate::{get_nodename, renderer};
 
 #[derive(Properties, Clone, PartialEq)]
-pub(crate) struct NodeStatus {}
+pub(crate) struct NodeStatus {
+    #[prop_or(60_000)]
+    /// The interval for refreshing the rrd data
+    pub rrd_interval: u32,
+}
 
 impl NodeStatus {
+    /// Create new [`NodeStatus`] panel.
     pub(crate) fn new() -> Self {
         yew::props!(Self {})
     }
@@ -31,20 +42,58 @@ impl From<NodeStatus> for VNode {
 
 enum Msg {
     Reload,
+    ReloadRrd,
+    UpdateRrdTimeframe(RRDTimeframe),
     Error(Error),
     RebootOrShutdown(NodePowerCommand),
     ShowSystemReport(bool),
     ShowPackageVersions(bool),
+    RrdLoadFinished(Result<Vec<PdmNodeDatapoint>, proxmox_client::Error>),
 }
 
 struct PdmNodeStatus {
+    time_data: Rc<Vec<i64>>,
+
+    cpu_data: Rc<Series>,
+    iowait_data: Rc<Series>,
+    load_data: Rc<Series>,
+    mem_data: Rc<Series>,
+    mem_total_data: Rc<Series>,
+    swap_data: Rc<Series>,
+    swap_total_data: Rc<Series>,
+    disk_usage_data: Rc<Series>,
+    disk_total_data: Rc<Series>,
+    disk_transfer_read_data: Rc<Series>,
+    disk_transfer_write_data: Rc<Series>,
+    disk_iops_read_data: Rc<Series>,
+    disk_iops_write_data: Rc<Series>,
+    cpu_pressure_some_data: Rc<Series>,
+    mem_pressure_some_data: Rc<Series>,
+    mem_pressure_full_data: Rc<Series>,
+    io_pressure_some_data: Rc<Series>,
+    io_pressure_full_data: Rc<Series>,
+    net_in: Rc<Series>,
+    net_out: Rc<Series>,
+
+    rrd_time_frame: RRDTimeframe,
     error: Option<Error>,
     abort_guard: Option<AsyncAbortGuard>,
     show_system_report: bool,
     show_package_versions: bool,
+
+    async_pool: AsyncPool,
+    _timeout: Option<gloo_timers::callback::Timeout>,
 }
 
 impl PdmNodeStatus {
+    async fn reload_rrd(rrd_time_frame: RRDTimeframe) -> Msg {
+        let res = crate::pdm_client()
+            .get_pdm_node_rrddata(rrd_time_frame.mode, rrd_time_frame.timeframe)
+            .await;
+
+        Msg::RrdLoadFinished(res)
+    }
+
     fn change_power_state(&mut self, ctx: &yew::Context<Self>, command: NodePowerCommand) {
         let link = ctx.link().clone();
         self.abort_guard.replace(AsyncAbortGuard::spawn(async move {
@@ -184,8 +233,37 @@ impl Component for PdmNodeStatus {
     type Message = Msg;
     type Properties = NodeStatus;
 
-    fn create(_ctx: &yew::Context<Self>) -> Self {
+    fn create(ctx: &yew::Context<Self>) -> Self {
+        ctx.link().send_message(Msg::ReloadRrd);
+
         Self {
+            time_data: Rc::new(Vec::new()),
+
+            cpu_data: empty_series(),
+            cpu_pressure_some_data: empty_series(),
+            mem_pressure_some_data: empty_series(),
+            mem_pressure_full_data: empty_series(),
+            io_pressure_some_data: empty_series(),
+            io_pressure_full_data: empty_series(),
+            iowait_data: empty_series(),
+            load_data: empty_series(),
+            mem_data: empty_series(),
+            mem_total_data: empty_series(),
+            swap_data: empty_series(),
+            swap_total_data: empty_series(),
+            net_in: empty_series(),
+            net_out: empty_series(),
+            disk_usage_data: empty_series(),
+            disk_total_data: empty_series(),
+            disk_transfer_read_data: empty_series(),
+            disk_transfer_write_data: empty_series(),
+            disk_iops_read_data: empty_series(),
+            disk_iops_write_data: empty_series(),
+
+            async_pool: AsyncPool::new(),
+            _timeout: None,
+
+            rrd_time_frame: RRDTimeframe::load(),
             error: None,
             abort_guard: None,
             show_system_report: false,
@@ -212,6 +290,121 @@ impl Component for PdmNodeStatus {
                 self.show_package_versions = show_package_versions;
                 true
             }
+            Msg::ReloadRrd => {
+                self._timeout = None;
+                let timeframe = self.rrd_time_frame;
+                self.async_pool.send_future(ctx.link().clone(), async move {
+                    Self::reload_rrd(timeframe).await
+                });
+                true
+            }
+            Msg::RrdLoadFinished(res) => match res {
+                Ok(data_points) => {
+                    self.error = None;
+                    let mut cpu_vec = Vec::with_capacity(data_points.len());
+                    let mut cpu_pressure_some_vec = Vec::with_capacity(data_points.len());
+                    let mut iowait_vec = Vec::with_capacity(data_points.len());
+                    let mut load_vec = Vec::with_capacity(data_points.len());
+                    let mut mem_vec = Vec::with_capacity(data_points.len());
+                    let mut mem_total_vec = Vec::with_capacity(data_points.len());
+                    let mut swap_vec = Vec::with_capacity(data_points.len());
+                    let mut swap_total_vec = Vec::with_capacity(data_points.len());
+                    let mut mem_pressure_some_vec = Vec::with_capacity(data_points.len());
+                    let mut mem_pressure_full_vec = Vec::with_capacity(data_points.len());
+                    let mut io_pressure_some_vec = Vec::with_capacity(data_points.len());
+                    let mut io_pressure_full_vec = Vec::with_capacity(data_points.len());
+                    let mut time_vec = Vec::with_capacity(data_points.len());
+                    let mut net_in_vec = Vec::with_capacity(data_points.len());
+                    let mut net_out_vec = Vec::with_capacity(data_points.len());
+                    let mut disk_usage_vec = Vec::with_capacity(data_points.len());
+                    let mut disk_total_vec = Vec::with_capacity(data_points.len());
+                    let mut disk_transfer_read_vec = Vec::with_capacity(data_points.len());
+                    let mut disk_transfer_write_vec = Vec::with_capacity(data_points.len());
+                    let mut disk_iops_read_vec = Vec::with_capacity(data_points.len());
+                    let mut disk_iops_write_vec = Vec::with_capacity(data_points.len());
+
+                    for data in data_points {
+                        cpu_vec.push(data.cpu_current.unwrap_or(f64::NAN));
+                        iowait_vec.push(data.cpu_iowait.unwrap_or(f64::NAN));
+                        load_vec.push(data.cpu_avg1.unwrap_or(f64::NAN));
+                        cpu_pressure_some_vec
+                            .push(data.cpu_pressure_some_avg10.unwrap_or(f64::NAN));
+                        mem_vec.push(data.mem_used.unwrap_or(f64::NAN));
+                        mem_total_vec.push(data.mem_total.unwrap_or(f64::NAN));
+                        swap_vec.push(data.swap_used.unwrap_or(f64::NAN));
+                        swap_total_vec.push(data.swap_total.unwrap_or(f64::NAN));
+                        mem_pressure_some_vec
+                            .push(data.mem_pressure_some_avg10.unwrap_or(f64::NAN));
+                        mem_pressure_full_vec
+                            .push(data.mem_pressure_full_avg10.unwrap_or(f64::NAN));
+                        net_in_vec.push(data.net_in.unwrap_or(f64::NAN));
+                        net_out_vec.push(data.net_out.unwrap_or(f64::NAN));
+                        io_pressure_some_vec.push(data.io_pressure_some_avg10.unwrap_or(f64::NAN));
+                        io_pressure_full_vec.push(data.io_pressure_full_avg10.unwrap_or(f64::NAN));
+
+                        disk_total_vec.push(data.disk_total.unwrap_or(f64::NAN));
+                        disk_usage_vec.push(data.disk_used.unwrap_or(f64::NAN));
+                        disk_transfer_read_vec.push(data.disk_read.unwrap_or(f64::NAN));
+                        disk_transfer_write_vec.push(data.disk_write.unwrap_or(f64::NAN));
+
+                        disk_iops_read_vec.push(data.disk_read_iops.unwrap_or(f64::NAN));
+                        disk_iops_write_vec.push(data.disk_write_iops.unwrap_or(f64::NAN));
+
+                        time_vec.push(data.time as i64);
+                    }
+
+                    self.cpu_data = Rc::new(Series::new(tr!("CPU usage"), cpu_vec));
+                    self.iowait_data = Rc::new(Series::new(tr!("IO delay"), iowait_vec));
+                    self.load_data = Rc::new(Series::new(tr!("Server Load"), load_vec));
+                    self.cpu_pressure_some_data =
+                        Rc::new(Series::new(tr!("Some"), cpu_pressure_some_vec));
+                    self.mem_data = Rc::new(Series::new(tr!("Used Memory"), mem_vec));
+                    self.mem_total_data = Rc::new(Series::new(tr!("Total Memory"), mem_total_vec));
+                    self.swap_data = Rc::new(Series::new(tr!("Used Swap"), swap_vec));
+                    self.swap_total_data = Rc::new(Series::new(tr!("Total Swap"), swap_total_vec));
+                    self.mem_pressure_some_data =
+                        Rc::new(Series::new(tr!("Some"), mem_pressure_some_vec));
+                    self.mem_pressure_full_data =
+                        Rc::new(Series::new(tr!("Full"), mem_pressure_full_vec));
+                    self.io_pressure_some_data =
+                        Rc::new(Series::new(tr!("Some"), io_pressure_some_vec));
+                    self.io_pressure_full_data =
+                        Rc::new(Series::new(tr!("Full"), io_pressure_full_vec));
+
+                    self.net_in = Rc::new(Series::new(tr!("Incoming"), net_in_vec));
+                    self.net_out = Rc::new(Series::new(tr!("Outgoing"), net_out_vec));
+
+                    self.disk_usage_data = Rc::new(Series::new(tr!("Used Disk"), disk_usage_vec));
+                    self.disk_total_data = Rc::new(Series::new(tr!("Total Disk"), disk_total_vec));
+                    self.disk_transfer_read_data =
+                        Rc::new(Series::new(tr!("Read"), disk_transfer_read_vec));
+                    self.disk_transfer_write_data =
+                        Rc::new(Series::new(tr!("Write"), disk_transfer_write_vec));
+                    self.disk_iops_read_data =
+                        Rc::new(Series::new(tr!("Read"), disk_iops_read_vec));
+                    self.disk_iops_write_data =
+                        Rc::new(Series::new(tr!("Write"), disk_iops_write_vec));
+
+                    self.time_data = Rc::new(time_vec);
+
+                    let link = ctx.link().clone();
+                    self._timeout = Some(gloo_timers::callback::Timeout::new(
+                        ctx.props().rrd_interval,
+                        move || link.send_message(Msg::ReloadRrd),
+                    ));
+
+                    true
+                }
+                Err(err) => {
+                    self.error = Some(err.into());
+                    true
+                }
+            },
+            Msg::UpdateRrdTimeframe(rrd_time_frame) => {
+                self.rrd_time_frame = rrd_time_frame;
+                ctx.link().send_message(Msg::ReloadRrd);
+                false
+            }
         }
     }
 
@@ -267,12 +460,113 @@ impl Component for PdmNodeStatus {
                     ),
             )
             .with_child(
-                Row::new()
+                Column::new()
                     .class("pwt-content-spacer-padding")
                     .class("pwt-content-spacer-colors")
                     .class("pwt-default-colors")
                     .class(pwt::css::FlexFit)
-                    .with_child(NodeStatusPanel::new().status_base_url("/nodes/localhost/status")),
+                    .with_child(
+                        NodeStatusPanel::new()
+                            .status_base_url("/nodes/localhost/status")
+                            .with_child(renderer::separator().padding_x(4))
+                            .with_optional_child(
+                                self.error
+                                    .as_ref()
+                                    .map(|err| pwt::widget::error_message(&err.to_string())),
+                            )
+                            .with_child(
+                                Row::new()
+                                    .padding_x(4)
+                                    .padding_y(1)
+                                    .class(JustifyContent::FlexEnd)
+                                    .with_child(
+                                        RRDTimeframeSelector::new().on_change(
+                                            ctx.link().callback(Msg::UpdateRrdTimeframe),
+                                        ),
+                                    ),
+                            )
+                            .with_child(
+                                RRDGrid::new()
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("CPU Usage"))
+                                            .render_value(renderer::rrd_value::render_cpu_usage)
+                                            .serie0(Some(self.cpu_data.clone()))
+                                            .serie1(Some(self.iowait_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Server Load"))
+                                            .render_value(renderer::rrd_value::render_load)
+                                            .serie0(Some(self.load_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Memory Usage"))
+                                            .binary(true)
+                                            .render_value(renderer::rrd_value::render_bytes)
+                                            .serie0(Some(self.mem_total_data.clone()))
+                                            .serie1(Some(self.mem_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Swap Usage"))
+                                            .binary(true)
+                                            .render_value(renderer::rrd_value::render_bytes)
+                                            .serie0(Some(self.swap_total_data.clone()))
+                                            .serie1(Some(self.swap_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Network Traffic"))
+                                            .binary(true)
+                                            .render_value(renderer::rrd_value::render_bandwidth)
+                                            .serie0(Some(self.net_in.clone()))
+                                            .serie1(Some(self.net_out.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("CPU Pressure Stall"))
+                                            .render_value(renderer::rrd_value::render_pressure)
+                                            .serie0(Some(self.cpu_pressure_some_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Memory Pressure Stall"))
+                                            .render_value(renderer::rrd_value::render_pressure)
+                                            .serie0(Some(self.mem_pressure_some_data.clone()))
+                                            .serie1(Some(self.mem_pressure_full_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("IO Pressure Stall"))
+                                            .render_value(renderer::rrd_value::render_pressure)
+                                            .serie0(Some(self.io_pressure_some_data.clone()))
+                                            .serie1(Some(self.io_pressure_full_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Root Disk Usage"))
+                                            .render_value(renderer::rrd_value::render_bytes)
+                                            .serie0(Some(self.disk_usage_data.clone()))
+                                            .serie1(Some(self.disk_total_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Root Disk Transfer Rate"))
+                                            .binary(true)
+                                            .render_value(renderer::rrd_value::render_bandwidth)
+                                            .serie0(Some(self.disk_transfer_read_data.clone()))
+                                            .serie1(Some(self.disk_transfer_write_data.clone())),
+                                    )
+                                    .with_child(
+                                        RRDGraph::new(self.time_data.clone())
+                                            .title(tr!("Root Disk IOPS"))
+                                            .serie0(Some(self.disk_iops_read_data.clone()))
+                                            .serie1(Some(self.disk_iops_write_data.clone())),
+                                    ),
+                            ),
+                    ),
             )
             .with_optional_child(
                 self.show_system_report
@@ -285,3 +579,7 @@ impl Component for PdmNodeStatus {
             .into()
     }
 }
+
+fn empty_series() -> Rc<Series> {
+    Rc::new(Series::new("", Vec::new()))
+}
diff --git a/ui/src/renderer.rs b/ui/src/renderer.rs
index 00c0720e..bfc059b3 100644
--- a/ui/src/renderer.rs
+++ b/ui/src/renderer.rs
@@ -111,3 +111,52 @@ pub(crate) fn render_title_row(title: String, icon: &str) -> Row {
         .with_child(Fa::new(icon))
         .with_child(title)
 }
+
+/// Helpers for rendering values in RRD graphs.
+pub mod rrd_value {
+    /// Render CPU usage in percent. `v` is multiplied by 100 to get the percent value.
+    pub fn render_cpu_usage(v: &f64) -> String {
+        if v.is_finite() {
+            format!("{:.1}%", v * 100.0)
+        } else {
+            v.to_string()
+        }
+    }
+
+    /// Render server load value.
+    pub fn render_load(v: &f64) -> String {
+        if v.is_finite() {
+            format!("{:.2}", v)
+        } else {
+            v.to_string()
+        }
+    }
+
+    /// Render a byte value.
+    pub fn render_bytes(v: &f64) -> String {
+        if v.is_finite() {
+            proxmox_human_byte::HumanByte::from(*v as u64).to_string()
+        } else {
+            v.to_string()
+        }
+    }
+
+    /// Render bandwidth.
+    pub fn render_bandwidth(v: &f64) -> String {
+        if v.is_finite() {
+            let bytes = proxmox_human_byte::HumanByte::from(*v as u64);
+            format!("{bytes}/s")
+        } else {
+            v.to_string()
+        }
+    }
+
+    /// Render pressure stall value.
+    pub fn render_pressure(v: &f64) -> String {
+        if v.is_finite() {
+            format!("{:.1}%", v)
+        } else {
+            v.to_string()
+        }
+    }
+}
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH datacenter-manager v3 11/11] ui: lxc/qemu/node: use RRD value render helpers
  2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
                   ` (9 preceding siblings ...)
  2026-04-13  8:58 ` [PATCH datacenter-manager v3 10/11] ui: node status: add RRD graphs for PDM host metrics Lukas Wagner
@ 2026-04-13  8:58 ` Lukas Wagner
  10 siblings, 0 replies; 12+ messages in thread
From: Lukas Wagner @ 2026-04-13  8:58 UTC (permalink / raw)
  To: pdm-devel

This changes the precision of CPU usage labels a tiny bit, before there
were two decimal places (24.42%) while now there is only one (24.3%).
Using one decimal place here seems a bit cleaner in the UI and the
additional precision is not very useful for these kinds of values.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
---
 ui/src/pbs/node/overview.rs | 29 +++++++----------------------
 ui/src/pve/lxc/overview.rs  | 34 +++++-----------------------------
 ui/src/pve/node/overview.rs | 29 +++++++----------------------
 ui/src/pve/qemu/overview.rs | 34 +++++-----------------------------
 4 files changed, 24 insertions(+), 102 deletions(-)

diff --git a/ui/src/pbs/node/overview.rs b/ui/src/pbs/node/overview.rs
index b63d45f2..4f874d85 100644
--- a/ui/src/pbs/node/overview.rs
+++ b/ui/src/pbs/node/overview.rs
@@ -17,7 +17,10 @@ use pwt::{
 use pbs_api_types::NodeStatus;
 use pdm_api_types::rrddata::PbsNodeDataPoint;
 
-use crate::{renderer::separator, LoadResult};
+use crate::{
+    renderer::{self, separator},
+    LoadResult,
+};
 
 #[derive(Clone, Debug, Eq, PartialEq, Properties)]
 pub struct PbsNodeOverviewPanel {
@@ -232,38 +235,20 @@ impl yew::Component for PbsNodeOverviewPanelComp {
                         .with_child(
                             RRDGraph::new(self.time_data.clone())
                                 .title(tr!("CPU Usage"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        format!("{:.2}%", v * 100.0)
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_cpu_usage)
                                 .serie0(Some(self.cpu_data.clone())),
                         )
                         .with_child(
                             RRDGraph::new(self.time_data.clone())
                                 .title(tr!("Server Load"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        format!("{:.2}", v)
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_load)
                                 .serie0(Some(self.load_data.clone())),
                         )
                         .with_child(
                             RRDGraph::new(self.time_data.clone())
                                 .title(tr!("Memory Usage"))
                                 .binary(true)
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bytes)
                                 .serie0(Some(self.mem_data.clone()))
                                 .serie1(Some(self.mem_total_data.clone())),
                         ),
diff --git a/ui/src/pve/lxc/overview.rs b/ui/src/pve/lxc/overview.rs
index 8c0196b3..5d70e16d 100644
--- a/ui/src/pve/lxc/overview.rs
+++ b/ui/src/pve/lxc/overview.rs
@@ -18,7 +18,7 @@ use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series};
 use pdm_api_types::{resource::PveLxcResource, rrddata::LxcDataPoint};
 use pdm_client::types::{IsRunning, LxcStatus};
 
-use crate::renderer::{separator, status_row};
+use crate::renderer::{self, separator, status_row};
 use crate::LoadResult;
 
 #[derive(Clone, Debug, Properties, PartialEq)]
@@ -338,25 +338,13 @@ impl yew::Component for LxcanelComp {
                         .with_child(
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("CPU Usage"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        format!("{:.2}%", v * 100.0)
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_cpu_usage)
                                 .serie0(Some(self.cpu.clone())),
                         )
                         .with_child(
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("Memory usage"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bytes)
                                 .serie0(Some(self.memory.clone()))
                                 .serie1(Some(self.memory_max.clone())),
                         )
@@ -364,13 +352,7 @@ impl yew::Component for LxcanelComp {
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("Network Traffic"))
                                 .binary(true)
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bandwidth)
                                 .serie0(Some(self.netin.clone()))
                                 .serie1(Some(self.netout.clone())),
                         )
@@ -378,13 +360,7 @@ impl yew::Component for LxcanelComp {
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("Disk I/O"))
                                 .binary(true)
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bandwidth)
                                 .serie0(Some(self.diskread.clone()))
                                 .serie1(Some(self.diskwrite.clone())),
                         ),
diff --git a/ui/src/pve/node/overview.rs b/ui/src/pve/node/overview.rs
index c07180b0..a0f92c38 100644
--- a/ui/src/pve/node/overview.rs
+++ b/ui/src/pve/node/overview.rs
@@ -17,7 +17,10 @@ use pwt::{
 use pdm_api_types::rrddata::NodeDataPoint;
 use pdm_client::types::NodeStatus;
 
-use crate::{renderer::separator, LoadResult};
+use crate::{
+    renderer::{self, separator},
+    LoadResult,
+};
 
 #[derive(Clone, Debug, Eq, PartialEq, Properties)]
 pub struct PveNodeOverviewPanel {
@@ -236,38 +239,20 @@ impl yew::Component for PveNodeOverviewPanelComp {
                         .with_child(
                             RRDGraph::new(self.time_data.clone())
                                 .title(tr!("CPU Usage"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        format!("{:.2}%", v * 100.0)
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_cpu_usage)
                                 .serie0(Some(self.cpu_data.clone())),
                         )
                         .with_child(
                             RRDGraph::new(self.time_data.clone())
                                 .title(tr!("Server Load"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        format!("{:.2}", v)
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_load)
                                 .serie0(Some(self.load_data.clone())),
                         )
                         .with_child(
                             RRDGraph::new(self.time_data.clone())
                                 .title(tr!("Memory Usage"))
                                 .binary(true)
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bytes)
                                 .serie0(Some(self.mem_total_data.clone()))
                                 .serie1(Some(self.mem_data.clone())),
                         ),
diff --git a/ui/src/pve/qemu/overview.rs b/ui/src/pve/qemu/overview.rs
index 6e601d00..bb7241ce 100644
--- a/ui/src/pve/qemu/overview.rs
+++ b/ui/src/pve/qemu/overview.rs
@@ -15,7 +15,7 @@ use pwt::AsyncPool;
 use pdm_api_types::{resource::PveQemuResource, rrddata::QemuDataPoint};
 use pdm_client::types::{IsRunning, QemuStatus};
 
-use crate::renderer::{separator, status_row};
+use crate::renderer::{self, separator, status_row};
 use crate::LoadResult;
 
 #[derive(Clone, Debug, Properties, PartialEq)]
@@ -347,25 +347,13 @@ impl yew::Component for QemuOverviewPanelComp {
                         .with_child(
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("CPU Usage"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        format!("{:.2}%", v * 100.0)
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_cpu_usage)
                                 .serie0(Some(self.cpu.clone())),
                         )
                         .with_child(
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("Memory usage"))
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bytes)
                                 .serie0(Some(self.memory.clone()))
                                 .serie1(Some(self.memory_max.clone())),
                         )
@@ -373,13 +361,7 @@ impl yew::Component for QemuOverviewPanelComp {
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("Network Traffic"))
                                 .binary(true)
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bandwidth)
                                 .serie0(Some(self.netin.clone()))
                                 .serie1(Some(self.netout.clone())),
                         )
@@ -387,13 +369,7 @@ impl yew::Component for QemuOverviewPanelComp {
                             RRDGraph::new(self.time.clone())
                                 .title(tr!("Disk I/O"))
                                 .binary(true)
-                                .render_value(|v: &f64| {
-                                    if v.is_finite() {
-                                        proxmox_human_byte::HumanByte::from(*v as u64).to_string()
-                                    } else {
-                                        v.to_string()
-                                    }
-                                })
+                                .render_value(renderer::rrd_value::render_bandwidth)
                                 .serie0(Some(self.diskread.clone()))
                                 .serie1(Some(self.diskwrite.clone())),
                         ),
-- 
2.47.3





^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-04-13  8:58 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-13  8:58 [PATCH datacenter-manager/proxmox-yew-comp v3 00/11] metric collection for the PDM host Lukas Wagner
2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 01/11] node status panel: add `children` property Lukas Wagner
2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 02/11] RRDGrid: fix size observer by attaching node reference to rendered container Lukas Wagner
2026-04-13  8:58 ` [PATCH proxmox-yew-comp v3 03/11] RRDGrid: add padding and increase gap between elements Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 04/11] metric collection: clarify naming for remote metric collection Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 05/11] metric collection: fix minor typo in error message Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 06/11] metric collection: collect PDM host metrics in a new collection task Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 07/11] api: fix /nodes/localhost/rrddata endpoint Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 08/11] pdm: node rrd data: rename 'total-time' to 'metric-collection-total-time' Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 09/11] pdm-api-types: add PDM host metric fields Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 10/11] ui: node status: add RRD graphs for PDM host metrics Lukas Wagner
2026-04-13  8:58 ` [PATCH datacenter-manager v3 11/11] ui: lxc/qemu/node: use RRD value render helpers Lukas Wagner

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal