public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view
@ 2025-10-17 12:09 Lukas Wagner
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 01/13] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
                   ` (12 more replies)
  0 siblings, 13 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:09 UTC (permalink / raw)
  To: pdm-devel

This series adds a new tab under "Remotes" called "Updates". It provides a
summary regarding the system update availability for all managed remotes.

Potential follow-up work: 
  - The "Refresh all" button, powered by the '/remote-updates/refresh' API
    only retrieves a fresh list of available updates at the moment, but does not
    invoke 'apt update' on the remote. The latter could be useful, either
    always or if explicitly requested, but we probably should 'stream'
    the node's task log to the PDM task log, so one can see the actual
    progress and/or any problems.

  - Remote task cache / task tracking needs a bit of work to correctly handle
    PBS tasks, therefore the 'Update' (which *does* invoke 'apt update' on the
    remote node) button for a *SINGLE* node does not work yet for PBS.

Changes since v1:
  - Incorporated review feedback from Shannon - see commits for details


proxmox-datacenter-manager:

Lukas Wagner (13):
  metric collection task: tests: add missing parameter for
    cluster_metric_export
  pdm-api-types: add types for remote upgrade summary
  remote updates: add cache for remote update availability
  api: add API for retrieving/refreshing the remote update summary
  unprivileged api daemon: tasks: add remote update refresh task
  pdm-client: add API methods for remote update summaries
  pbs-client: add bindings for APT-related API calls
  task cache: use separate functions for tracking PVE and PBS tasks
  remote updates: add support for PBS remotes
  api: add APT endpoints for PBS remotes
  ui: add remote update view
  ui: show new remote update view in the 'Remotes' section
  remote updates: avoid unnecessary clone

 lib/pdm-api-types/src/lib.rs                  |   2 +
 lib/pdm-api-types/src/remote_updates.rs       | 119 ++++
 lib/pdm-client/src/lib.rs                     |  22 +
 server/src/api/mod.rs                         |   3 +
 server/src/api/pbs/mod.rs                     |  19 +-
 server/src/api/pbs/node.rs                    |   9 +
 server/src/api/pve/apt.rs                     | 119 ----
 server/src/api/pve/mod.rs                     |   4 +-
 server/src/api/pve/node.rs                    |   2 +-
 server/src/api/remote_updates.rs              | 222 ++++++++
 server/src/bin/proxmox-datacenter-api/main.rs |   1 +
 .../bin/proxmox-datacenter-api/tasks/mod.rs   |   1 +
 .../tasks/remote_updates.rs                   |  44 ++
 .../src/metric_collection/collection_task.rs  |   1 +
 server/src/pbs_client.rs                      |  51 ++
 server/src/remote_tasks/mod.rs                |  45 +-
 server/src/remote_updates.rs                  | 233 +++++++-
 ui/src/remotes/mod.rs                         |  10 +
 ui/src/remotes/updates.rs                     | 530 ++++++++++++++++++
 19 files changed, 1293 insertions(+), 144 deletions(-)
 create mode 100644 lib/pdm-api-types/src/remote_updates.rs
 create mode 100644 server/src/api/pbs/node.rs
 delete mode 100644 server/src/api/pve/apt.rs
 create mode 100644 server/src/api/remote_updates.rs
 create mode 100644 server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
 create mode 100644 ui/src/remotes/updates.rs


Summary over all repositories:
  19 files changed, 1293 insertions(+), 144 deletions(-)

-- 
Generated by murpp 0.9.0


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 01/13] metric collection task: tests: add missing parameter for cluster_metric_export
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
@ 2025-10-17 12:09 ` Lukas Wagner
  2025-10-21 19:24   ` [pdm-devel] applied: " Thomas Lamprecht
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 02/13] pdm-api-types: add types for remote upgrade summary Lukas Wagner
                   ` (11 subsequent siblings)
  12 siblings, 1 reply; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:09 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/metric_collection/collection_task.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/server/src/metric_collection/collection_task.rs b/server/src/metric_collection/collection_task.rs
index cfa54283..a6c84433 100644
--- a/server/src/metric_collection/collection_task.rs
+++ b/server/src/metric_collection/collection_task.rs
@@ -457,6 +457,7 @@ pub(super) mod tests {
             &self,
             _history: Option<bool>,
             _local_only: Option<bool>,
+            _node_list: Option<String>,
             start_time: Option<i64>,
         ) -> Result<ClusterMetrics, proxmox_client::Error> {
             if self.fail {
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 02/13] pdm-api-types: add types for remote upgrade summary
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 01/13] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
@ 2025-10-17 12:09 ` Lukas Wagner
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 03/13] remote updates: add cache for remote update availability Lukas Wagner
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:09 UTC (permalink / raw)
  To: pdm-devel

This type contains the number of available updates per remote node,
without any details. This is useful for a global "give me the update
availability for all managed nodes" API endpoint.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---

Notes:
    Changes since v1:
      - Changed order/style of use statements
      - Make RemoteUpdateSummaryWrapper a tuple struct
      - Make NodeUpdateSummaryWrapper a tuple struct
    
    Thank you Shannon!

 lib/pdm-api-types/src/lib.rs            |   2 +
 lib/pdm-api-types/src/remote_updates.rs | 119 ++++++++++++++++++++++++
 2 files changed, 121 insertions(+)
 create mode 100644 lib/pdm-api-types/src/remote_updates.rs

diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index a3566142..2fb61ef7 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -96,6 +96,8 @@ pub use openid::*;
 
 pub mod remotes;
 
+pub mod remote_updates;
+
 pub mod resource;
 
 pub mod rrddata;
diff --git a/lib/pdm-api-types/src/remote_updates.rs b/lib/pdm-api-types/src/remote_updates.rs
new file mode 100644
index 00000000..e42b6a96
--- /dev/null
+++ b/lib/pdm-api-types/src/remote_updates.rs
@@ -0,0 +1,119 @@
+use std::collections::HashMap;
+use std::ops::{Deref, DerefMut};
+
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, ApiType, ObjectSchema};
+
+use crate::remotes::RemoteType;
+
+#[api]
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+/// Update summary for all remotes.
+pub struct UpdateSummary {
+    /// Map containing the update summary each remote.
+    pub remotes: RemoteUpdateSummaryWrapper,
+}
+
+// This is a hack to allow actual 'maps' (mapping remote name to per-remote data)
+// within the realms of our API macro.
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct RemoteUpdateSummaryWrapper(HashMap<String, RemoteUpdateSummary>);
+
+impl ApiType for RemoteUpdateSummaryWrapper {
+    const API_SCHEMA: proxmox_schema::Schema =
+        ObjectSchema::new("Map of per-remote update summaries", &[])
+            .additional_properties(true)
+            .schema();
+}
+
+impl Deref for RemoteUpdateSummaryWrapper {
+    type Target = HashMap<String, RemoteUpdateSummary>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for RemoteUpdateSummaryWrapper {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+/// Update summary for a single remote.
+pub struct RemoteUpdateSummary {
+    /// Map containing the update summary for each node of this remote.
+    pub nodes: NodeUpdateSummaryWrapper,
+    pub remote_type: RemoteType,
+    pub status: RemoteUpdateStatus,
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Status for the entire remote.
+pub enum RemoteUpdateStatus {
+    /// Successfully polled remote.
+    Success,
+    /// Remote could not be polled.
+    Error,
+    /// Remote has not been polled yet.
+    Unknown,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct NodeUpdateSummaryWrapper(HashMap<String, NodeUpdateSummary>);
+
+impl ApiType for NodeUpdateSummaryWrapper {
+    const API_SCHEMA: proxmox_schema::Schema =
+        ObjectSchema::new("Map of per-node update summaries", &[])
+            .additional_properties(true)
+            .schema();
+}
+
+impl Deref for NodeUpdateSummaryWrapper {
+    type Target = HashMap<String, NodeUpdateSummary>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for NodeUpdateSummaryWrapper {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Status for the entire remote.
+pub enum NodeUpdateStatus {
+    /// Successfully polled node.
+    Success,
+    /// Node could not be polled.
+    Error,
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Per-node update summary.
+pub struct NodeUpdateSummary {
+    /// Number of available updates.
+    pub number_of_updates: u32,
+    /// Unix timestamp of the last refresh.
+    pub last_refresh: i64,
+    /// Status
+    pub status: NodeUpdateStatus,
+    /// Status message (e.g. error message)
+    pub status_message: Option<String>,
+}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 03/13] remote updates: add cache for remote update availability
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 01/13] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 02/13] pdm-api-types: add types for remote upgrade summary Lukas Wagner
@ 2025-10-17 12:09 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 04/13] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:09 UTC (permalink / raw)
  To: pdm-devel

The cache will be filled by either the `refresh_summary_cache` function,
or when the list of updates for a single node is requested.

The cache contains only the summary (number of updates) and not the full
list of packages. We can always choose to add the latter later if we
need it.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/remote_updates.rs | 192 +++++++++++++++++++++++++++++++++--
 1 file changed, 186 insertions(+), 6 deletions(-)

diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
index f833062c..ab5845bd 100644
--- a/server/src/remote_updates.rs
+++ b/server/src/remote_updates.rs
@@ -1,20 +1,53 @@
+use std::fs::File;
+use std::io::ErrorKind;
+
 use anyhow::{bail, Error};
-use pdm_api_types::RemoteUpid;
+use serde::{Deserialize, Serialize};
 
 use proxmox_apt_api_types::APTUpdateInfo;
 
+use pdm_api_types::remote_updates::{
+    NodeUpdateStatus, NodeUpdateSummary, NodeUpdateSummaryWrapper, RemoteUpdateStatus,
+    RemoteUpdateSummary, UpdateSummary,
+};
 use pdm_api_types::remotes::{Remote, RemoteType};
+use pdm_api_types::RemoteUpid;
+use pdm_buildcfg::PDM_CACHE_DIR_M;
 
 use crate::api::pve::new_remote_upid;
 use crate::connection;
+use crate::parallel_fetcher::{NodeResults, ParallelFetcher};
+
+pub const UPDATE_CACHE: &str = concat!(PDM_CACHE_DIR_M!(), "/remote-updates.json");
+
+#[derive(Clone, Default, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+struct NodeUpdateInfo {
+    updates: Vec<APTUpdateInfo>,
+    last_refresh: i64,
+}
+
+impl From<NodeUpdateInfo> for NodeUpdateSummary {
+    fn from(value: NodeUpdateInfo) -> Self {
+        Self {
+            number_of_updates: value.updates.len() as u32,
+            last_refresh: value.last_refresh,
+            status: NodeUpdateStatus::Success,
+            status_message: None,
+        }
+    }
+}
 
 /// Return a list of available updates for a given remote node.
 pub async fn list_available_updates(
     remote: Remote,
     node: &str,
 ) -> Result<Vec<APTUpdateInfo>, Error> {
-    let updates = fetch_available_updates(remote, node.to_string()).await?;
-    Ok(updates)
+    let updates = fetch_available_updates((), remote.clone(), node.to_string()).await?;
+
+    update_cached_summary_for_node(remote, node.into(), updates.clone().into()).await?;
+
+    Ok(updates.updates)
 }
 
 /// Trigger `apt update` on a remote node.
@@ -52,10 +85,154 @@ pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Resul
     }
 }
 
-async fn fetch_available_updates(
+/// Get update summary for all managed remotes.
+pub fn get_available_updates_summary() -> Result<UpdateSummary, Error> {
+    let (config, _digest) = pdm_config::remotes::config()?;
+
+    let cache_content = get_cached_summary_or_default()?;
+
+    let mut summary = UpdateSummary::default();
+
+    for (remote_name, remote) in &config {
+        match cache_content.remotes.get(remote_name) {
+            Some(remote_summary) => {
+                summary
+                    .remotes
+                    .insert(remote_name.into(), remote_summary.clone());
+            }
+            None => {
+                summary.remotes.insert(
+                    remote_name.into(),
+                    RemoteUpdateSummary {
+                        nodes: NodeUpdateSummaryWrapper::default(),
+                        remote_type: remote.ty,
+                        status: RemoteUpdateStatus::Unknown,
+                    },
+                );
+            }
+        }
+    }
+
+    Ok(summary)
+}
+
+fn get_cached_summary_or_default() -> Result<UpdateSummary, Error> {
+    match File::open(UPDATE_CACHE) {
+        Ok(file) => {
+            let content = match serde_json::from_reader(file) {
+                Ok(cache_content) => cache_content,
+                Err(err) => {
+                    log::error!("failed to deserialize remote update cache: {err:#}");
+                    Default::default()
+                }
+            };
+
+            Ok(content)
+        }
+        Err(err) if err.kind() == ErrorKind::NotFound => Ok(Default::default()),
+        Err(err) => Err(err.into()),
+    }
+}
+
+async fn update_cached_summary_for_node(
     remote: Remote,
     node: String,
-) -> Result<Vec<APTUpdateInfo>, Error> {
+    node_data: NodeUpdateSummary,
+) -> Result<(), Error> {
+    let mut file = File::open(UPDATE_CACHE)?;
+    let mut cache_content: UpdateSummary = serde_json::from_reader(&mut file)?;
+    let remote_entry =
+        cache_content
+            .remotes
+            .entry(remote.id)
+            .or_insert_with(|| RemoteUpdateSummary {
+                nodes: Default::default(),
+                remote_type: remote.ty,
+                status: RemoteUpdateStatus::Success,
+            });
+
+    remote_entry.nodes.insert(node, node_data);
+
+    let options = proxmox_product_config::default_create_options();
+    proxmox_sys::fs::replace_file(
+        UPDATE_CACHE,
+        &serde_json::to_vec(&cache_content)?,
+        options,
+        true,
+    )?;
+
+    Ok(())
+}
+
+/// Refresh the remote update cache.
+pub async fn refresh_update_summary_cache(remotes: Vec<Remote>) -> Result<(), Error> {
+    let fetcher = ParallelFetcher::new(());
+
+    let fetch_results = fetcher
+        .do_for_all_remote_nodes(remotes.clone().into_iter(), fetch_available_updates)
+        .await;
+
+    let mut content = get_cached_summary_or_default()?;
+
+    for (remote_name, result) in fetch_results.remote_results {
+        let entry = content
+            .remotes
+            .entry(remote_name.clone())
+            .or_insert_with(|| {
+                // unwrap: remote name came from the same config, should be safe.
+                // TODO: Include type in ParallelFetcher results - should be much more efficient.
+                let remote_type = remotes.iter().find(|r| r.id == remote_name).unwrap().ty;
+
+                RemoteUpdateSummary {
+                    nodes: Default::default(),
+                    remote_type,
+                    status: RemoteUpdateStatus::Success,
+                }
+            });
+
+        match result {
+            Ok(remote_result) => {
+                for (node_name, node_result) in remote_result.node_results {
+                    match node_result {
+                        Ok(NodeResults { data, .. }) => {
+                            entry.nodes.insert(node_name, data.into());
+                        }
+                        Err(err) => {
+                            // Could not fetch updates from node
+                            entry.nodes.insert(
+                                node_name.clone(),
+                                NodeUpdateSummary {
+                                    number_of_updates: 0,
+                                    last_refresh: 0,
+                                    status: NodeUpdateStatus::Error,
+                                    status_message: Some(format!("{err:#}")),
+                                },
+                            );
+                            log::error!(
+                                "could not fetch available updates from node '{node_name}': {err}"
+                            );
+                        }
+                    }
+                }
+            }
+            Err(err) => {
+                entry.status = RemoteUpdateStatus::Error;
+                log::error!("could not fetch available updates from remote '{remote_name}': {err}");
+            }
+        }
+    }
+
+    let options = proxmox_product_config::default_create_options();
+    proxmox_sys::fs::replace_file(UPDATE_CACHE, &serde_json::to_vec(&content)?, options, true)?;
+
+    Ok(())
+}
+
+async fn fetch_available_updates(
+    _context: (),
+    remote: Remote,
+    node: String,
+) -> Result<NodeUpdateInfo, Error> {
     match remote.ty {
         RemoteType::Pve => {
             let client = connection::make_pve_client(&remote)?;
@@ -67,7 +244,10 @@ async fn fetch_available_updates(
                 .map(map_pve_update_info)
                 .collect();
 
-            Ok(updates)
+            Ok(NodeUpdateInfo {
+                last_refresh: proxmox_time::epoch_i64(),
+                updates,
+            })
         }
         RemoteType::Pbs => bail!("PBS is not supported yet"),
     }
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 04/13] api: add API for retrieving/refreshing the remote update summary
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (2 preceding siblings ...)
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 03/13] remote updates: add cache for remote update availability Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 05/13] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

This commit adds two new endpoints, namely
  GET  /remote-updates/summary
  POST /remote-updates/refresh

The first one is used to retrieve the update summary (the data is taken
from the cache), the second one can be used to proactively refresh the
summary in the cache (starts a worker task, since this could take a
while). Note that we only retrieve the up-to-date list of packages from
the remote, but do *not* trigger an `apt update` right now. Could make
sense to do the latter as well, but then we probably should
stream/forward the task logs for the update task from the node to the
native PDM task; something we can rather implement later.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---

Notes:
    Changes since v1:
      - Fail with FORBIDDEN instead of UNAUTHORIZED in case of lacking
        permissions

 server/src/api/mod.rs            |   3 +
 server/src/api/remote_updates.rs | 108 +++++++++++++++++++++++++++++++
 2 files changed, 111 insertions(+)
 create mode 100644 server/src/api/remote_updates.rs

diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index 02ee0ecf..6a7a65a2 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -14,6 +14,7 @@ pub mod nodes;
 pub mod pbs;
 pub mod pve;
 pub mod remote_tasks;
+pub mod remote_updates;
 pub mod remotes;
 pub mod resources;
 mod rrd_common;
@@ -31,6 +32,8 @@ const SUBDIRS: SubdirMap = &sorted!([
     ("resources", &resources::ROUTER),
     ("nodes", &nodes::ROUTER),
     ("remote-tasks", &remote_tasks::ROUTER),
+    // TODO: There might be a better place for this endpoint.
+    ("remote-updates", &remote_updates::ROUTER),
     ("sdn", &sdn::ROUTER),
     ("version", &Router::new().get(&API_METHOD_VERSION)),
 ]);
diff --git a/server/src/api/remote_updates.rs b/server/src/api/remote_updates.rs
new file mode 100644
index 00000000..1feef280
--- /dev/null
+++ b/server/src/api/remote_updates.rs
@@ -0,0 +1,108 @@
+//! API for getting a remote update update summary.
+
+use anyhow::Error;
+
+use pdm_api_types::remote_updates::UpdateSummary;
+use pdm_api_types::remotes::Remote;
+use pdm_api_types::{PRIV_RESOURCE_MODIFY, UPID};
+use proxmox_access_control::CachedUserInfo;
+use proxmox_rest_server::WorkerTask;
+use proxmox_router::{
+    http_bail, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
+};
+use proxmox_schema::api;
+use proxmox_sortable_macro::sortable;
+
+use crate::remote_updates;
+
+pub const ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(SUBDIRS))
+    .subdirs(SUBDIRS);
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([
+    ("summary", &Router::new().get(&API_METHOD_UPDATE_SUMMARY)),
+    (
+        "refresh",
+        &Router::new().post(&API_METHOD_REFRESH_REMOTE_UPDATE_SUMMARIES)
+    ),
+]);
+
+#[api(
+    access: {
+        permission: &Permission::Anybody,
+        description: "Resource.Modify privileges are needed on /resource/{remote}",
+    },
+)]
+/// Return available update summary for managed remote nodes.
+pub fn update_summary(rpcenv: &mut dyn RpcEnvironment) -> Result<UpdateSummary, Error> {
+    let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
+    let user_info = CachedUserInfo::new()?;
+
+    if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
+        http_bail!(FORBIDDEN, "user has no access to resources");
+    }
+
+    let mut update_summary = remote_updates::get_available_updates_summary()?;
+
+    update_summary.remotes.retain(|remote_name, _| {
+        user_info
+            .check_privs(
+                &auth_id,
+                &["resource", remote_name],
+                PRIV_RESOURCE_MODIFY,
+                false,
+            )
+            .is_ok()
+    });
+
+    Ok(update_summary)
+}
+
+#[api(
+    access: {
+        permission: &Permission::Anybody,
+        description: "Resource.Modify privileges are needed on /resource/{remote}",
+    },
+)]
+/// Refresh the update summary of all remotes.
+pub fn refresh_remote_update_summaries(rpcenv: &mut dyn RpcEnvironment) -> Result<UPID, Error> {
+    let (config, _digest) = pdm_config::remotes::config()?;
+
+    let auth_id = rpcenv.get_auth_id().unwrap().parse()?;
+    let user_info = CachedUserInfo::new()?;
+
+    if !user_info.any_privs_below(&auth_id, &["resource"], PRIV_RESOURCE_MODIFY)? {
+        http_bail!(FORBIDDEN, "user has no access to resources");
+    }
+
+    let remotes: Vec<Remote> = config
+        .into_iter()
+        .filter_map(|(remote_name, remote)| {
+            user_info
+                .check_privs(
+                    &auth_id,
+                    &["resource", &remote_name],
+                    PRIV_RESOURCE_MODIFY,
+                    false,
+                )
+                .is_ok()
+                .then_some(remote)
+        })
+        .collect();
+
+    let upid_str = WorkerTask::spawn(
+        "refresh-remote-updates",
+        None,
+        auth_id.to_string(),
+        true,
+        |_worker| async {
+            // TODO: Add more verbose logging per remote/node, so we can actually see something
+            // interesting in the task log.
+            remote_updates::refresh_update_summary_cache(remotes).await?;
+            Ok(())
+        },
+    )?;
+
+    upid_str.parse()
+}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 05/13] unprivileged api daemon: tasks: add remote update refresh task
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (3 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 04/13] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 06/13] pdm-client: add API methods for remote update summaries Lukas Wagner
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

This commit adds a very simple refresh tasks that runs at relatively
long interval. The task polls all remotes for available updates and puts
the results in the update cache.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/bin/proxmox-datacenter-api/main.rs |  1 +
 .../bin/proxmox-datacenter-api/tasks/mod.rs   |  1 +
 .../tasks/remote_updates.rs                   | 44 +++++++++++++++++++
 3 files changed, 46 insertions(+)
 create mode 100644 server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs

diff --git a/server/src/bin/proxmox-datacenter-api/main.rs b/server/src/bin/proxmox-datacenter-api/main.rs
index c0f0e3a4..420a3b4d 100644
--- a/server/src/bin/proxmox-datacenter-api/main.rs
+++ b/server/src/bin/proxmox-datacenter-api/main.rs
@@ -377,6 +377,7 @@ async fn run(debug: bool) -> Result<(), Error> {
     tasks::remote_node_mapping::start_task();
     resource_cache::start_task();
     tasks::remote_tasks::start_task()?;
+    tasks::remote_updates::start_task()?;
 
     server.await?;
     log::info!("server shutting down, waiting for active workers to complete");
diff --git a/server/src/bin/proxmox-datacenter-api/tasks/mod.rs b/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
index a6b1f439..f4d1d3a1 100644
--- a/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
+++ b/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
@@ -1,2 +1,3 @@
 pub mod remote_node_mapping;
 pub mod remote_tasks;
+pub mod remote_updates;
diff --git a/server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs b/server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
new file mode 100644
index 00000000..bd9e178d
--- /dev/null
+++ b/server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs
@@ -0,0 +1,44 @@
+use anyhow::Error;
+
+use server::{remote_updates, task_utils};
+
+const REFRESH_TIME: u64 = 6 * 3600;
+
+/// Start the remote task fetching task
+pub fn start_task() -> Result<(), Error> {
+    tokio::spawn(async move {
+        let task_scheduler = std::pin::pin!(RemoteUpdateRefreshTask {}.run());
+        let abort_future = std::pin::pin!(proxmox_daemon::shutdown_future());
+        futures::future::select(task_scheduler, abort_future).await;
+    });
+
+    Ok(())
+}
+
+struct RemoteUpdateRefreshTask {}
+
+impl RemoteUpdateRefreshTask {
+    async fn run(self) {
+        loop {
+            self.refresh().await;
+            self.wait_for_refresh().await;
+        }
+    }
+
+    async fn refresh(&self) {
+        if let Err(err) = self.do_refresh().await {
+            log::error!("could not refresh remote update cache: {err:#}");
+        }
+    }
+
+    async fn do_refresh(&self) -> Result<(), Error> {
+        let (config, _digest) = tokio::task::spawn_blocking(pdm_config::remotes::config).await??;
+        remote_updates::refresh_update_summary_cache(config.into_iter().map(|(_, r)| r).collect())
+            .await
+    }
+
+    async fn wait_for_refresh(&self) {
+        let instant = task_utils::next_aligned_instant(REFRESH_TIME);
+        tokio::time::sleep_until(instant.into()).await;
+    }
+}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 06/13] pdm-client: add API methods for remote update summaries
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (4 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 05/13] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 07/13] pbs-client: add bindings for APT-related API calls Lukas Wagner
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

Adds methods to retrieve and refresh the update summary.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 lib/pdm-client/src/lib.rs | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 2f36fab1..0cab7691 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -1109,6 +1109,28 @@ impl<T: HttpApiClient> PdmClient<T> {
         }
         Ok(self.0.post(&path, &params).await?.expect_json()?.data)
     }
+
+    /// Get remote update summary.
+    pub async fn remote_update_summary(
+        &self,
+    ) -> Result<pdm_api_types::remote_updates::UpdateSummary, Error> {
+        Ok(self
+            .0
+            .get("/api2/extjs/remote-updates/summary")
+            .await?
+            .expect_json()?
+            .data)
+    }
+
+    /// Refresh remote update summary.
+    pub async fn refresh_remote_update_summary(&self) -> Result<pdm_api_types::UPID, Error> {
+        Ok(self
+            .0
+            .post_without_body("/api2/extjs/remote-updates/refresh")
+            .await?
+            .expect_json()?
+            .data)
+    }
 }
 
 /// Builder for migration parameters.
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 07/13] pbs-client: add bindings for APT-related API calls
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (5 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 06/13] pdm-client: add API methods for remote update summaries Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 08/13] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

This commit adds bindings for the following API methods:
  GET /node/<node>/apt/update
  POST /node/<node>/apt/update
  GET /node/<node>/apt/changelog

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/pbs_client.rs | 51 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/server/src/pbs_client.rs b/server/src/pbs_client.rs
index 433b5d4b..49087360 100644
--- a/server/src/pbs_client.rs
+++ b/server/src/pbs_client.rs
@@ -114,6 +114,17 @@ pub struct DatstoreListNamespaces {
     pub max_depth: Option<usize>,
 }
 
+#[api]
+/// Parameters for updating the APT database
+#[derive(serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct AptUpdateParams {
+    /// Send notification in case of new updates.
+    pub notify: Option<bool>,
+    /// Don't show progress information in the output.
+    pub quiet: Option<bool>,
+}
+
 impl PbsClient {
     /// API version details, including some parts of the global datacenter config.
     pub async fn version(&self) -> Result<pve_api_types::VersionResponse, Error> {
@@ -267,6 +278,46 @@ impl PbsClient {
             .expect_json()?
             .data)
     }
+
+    /// Return a list of available system updates.
+    pub async fn list_available_updates(&self) -> Result<Vec<pbs_api_types::APTUpdateInfo>, Error> {
+        Ok(self
+            .0
+            .get("/api2/extjs/nodes/localhost/apt/update")
+            .await?
+            .expect_json()?
+            .data)
+    }
+
+    /// Update the APT database.
+    pub async fn update_apt_database(
+        &self,
+        params: AptUpdateParams,
+    ) -> Result<pbs_api_types::UPID, Error> {
+        Ok(self
+            .0
+            .post("/api2/extjs/nodes/localhost/apt/update", &params)
+            .await?
+            .expect_json()?
+            .data)
+    }
+
+    /// Get changelog for a single package.
+    ///
+    /// `package`: Package name to get the changelog of.
+    /// `version`: Package version to get changelog of. Omit to use candidate version.
+    pub async fn get_package_changelog(
+        &self,
+        package: String,
+        version: Option<String>,
+    ) -> Result<String, Error> {
+        let path = ApiPathBuilder::new("/api2/extjs/nodes/localhost/apt/changelog")
+            .arg("name", &package)
+            .maybe_arg("version", &version)
+            .build();
+
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
 }
 
 #[derive(Deserialize)]
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 08/13] task cache: use separate functions for tracking PVE and PBS tasks
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (6 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 07/13] pbs-client: add bindings for APT-related API calls Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 09/13] remote updates: add support for PBS remotes Lukas Wagner
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

At the callsite we always know which type of remote it is, so there is
no need to deduce the type from the UPID itself.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/api/pve/mod.rs      |  3 +--
 server/src/remote_tasks/mod.rs | 45 ++++++++++++++++++++++++++++------
 2 files changed, 39 insertions(+), 9 deletions(-)

diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
index 0de82323..a94ab8d0 100644
--- a/server/src/api/pve/mod.rs
+++ b/server/src/api/pve/mod.rs
@@ -84,8 +84,7 @@ const STATUS_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_STATUS);
 
 // converts a remote + PveUpid into a RemoteUpid and starts tracking it
 pub async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
-    let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
-    remote_tasks::track_running_task(remote_upid.clone()).await?;
+    let remote_upid = remote_tasks::track_running_pve_task(remote, upid).await?;
     Ok(remote_upid)
 }
 
diff --git a/server/src/remote_tasks/mod.rs b/server/src/remote_tasks/mod.rs
index b3adebe2..c4939742 100644
--- a/server/src/remote_tasks/mod.rs
+++ b/server/src/remote_tasks/mod.rs
@@ -126,22 +126,53 @@ pub async fn get_tasks(
     .await?
 }
 
-/// Insert a newly created tasks into the list of tracked tasks.
+/// Insert a newly created PVE task into the list of tracked tasks.
 ///
 /// Any tracked task will be polled with a short interval until the task
 /// has finished.
-pub async fn track_running_task(task: RemoteUpid) -> Result<(), Error> {
+///
+/// This function returns the [`RemoteUpid`] of the tracked PVE task.
+pub async fn track_running_pve_task(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
     tokio::task::spawn_blocking(move || {
+        let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
         let cache = get_cache()?.write()?;
-        // TODO:: Handle PBS tasks correctly.
-        let pve_upid: pve_api_types::PveUpid = task.upid.parse()?;
+
         let task = TaskCacheItem {
-            upid: task.clone(),
-            starttime: pve_upid.starttime,
+            upid: remote_upid.clone(),
+            starttime: upid.starttime,
             status: None,
             endtime: None,
         };
-        cache.add_tracked_task(task)
+        cache.add_tracked_task(task)?;
+
+        Ok(remote_upid)
+    })
+    .await?
+}
+
+/// Insert a newly created PBS task into the list of tracked tasks.
+///
+/// Any tracked task will be polled with a short interval until the task
+/// has finished.
+///
+/// This function returns the [`RemoteUpid`] of the tracked PBS task.
+pub async fn track_running_pbs_task(
+    remote: String,
+    upid: pbs_api_types::UPID,
+) -> Result<RemoteUpid, Error> {
+    tokio::task::spawn_blocking(move || {
+        let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
+        let cache = get_cache()?.write()?;
+
+        let task = TaskCacheItem {
+            upid: remote_upid.clone(),
+            starttime: upid.starttime,
+            status: None,
+            endtime: None,
+        };
+        cache.add_tracked_task(task)?;
+
+        Ok(remote_upid)
     })
     .await?
 }
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 09/13] remote updates: add support for PBS remotes
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (7 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 08/13] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 10/13] api: add APT endpoints " Lukas Wagner
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

The 'update_apt_database' function starts a new tracked remote task. The
remote task infrastructure needs a bit more work to work with PBS UPIDs,
so the code is commented out for now.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/api/pbs/mod.rs    | 15 ++++++++++++++-
 server/src/remote_updates.rs | 37 +++++++++++++++++++++++++++++++-----
 2 files changed, 46 insertions(+), 6 deletions(-)

diff --git a/server/src/api/pbs/mod.rs b/server/src/api/pbs/mod.rs
index ad82ab38..56192c01 100644
--- a/server/src/api/pbs/mod.rs
+++ b/server/src/api/pbs/mod.rs
@@ -9,13 +9,17 @@ use proxmox_sortable_macro::sortable;
 use pdm_api_types::remotes::{
     NodeUrl, Remote, RemoteListEntry, RemoteType, TlsProbeOutcome, REMOTE_ID_SCHEMA,
 };
-use pdm_api_types::{Authid, HOST_OPTIONAL_PORT_FORMAT, PRIV_RESOURCE_AUDIT, PRIV_SYS_MODIFY};
+use pdm_api_types::{
+    Authid, RemoteUpid, HOST_OPTIONAL_PORT_FORMAT, PRIV_RESOURCE_AUDIT, PRIV_SYS_MODIFY,
+};
 
 use crate::{
     connection::{self, probe_tls_connection},
     pbs_client::{self, get_remote, PbsClient},
 };
 
+use crate::remote_tasks;
+
 mod rrddata;
 
 pub const ROUTER: Router = Router::new()
@@ -65,6 +69,15 @@ const DATASTORE_ITEM_SUBDIRS: SubdirMap = &sorted!([
     ),
 ]);
 
+// converts a remote + pbs_api_types::UPID into a RemoteUpid and starts tracking it
+pub async fn new_remote_upid(
+    remote: String,
+    upid: pbs_api_types::UPID,
+) -> Result<RemoteUpid, Error> {
+    let remote_upid = remote_tasks::track_running_pbs_task(remote, upid).await?;
+    Ok(remote_upid)
+}
+
 #[api(
     returns: {
         type: Array,
diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
index ab5845bd..4c70495b 100644
--- a/server/src/remote_updates.rs
+++ b/server/src/remote_updates.rs
@@ -14,7 +14,6 @@ use pdm_api_types::remotes::{Remote, RemoteType};
 use pdm_api_types::RemoteUpid;
 use pdm_buildcfg::PDM_CACHE_DIR_M;
 
-use crate::api::pve::new_remote_upid;
 use crate::connection;
 use crate::parallel_fetcher::{NodeResults, ParallelFetcher};
 
@@ -64,9 +63,22 @@ pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUp
             };
             let upid = client.update_apt_database(node, params).await?;
 
-            new_remote_upid(remote.id.clone(), upid).await
+            crate::api::pve::new_remote_upid(remote.id.clone(), upid).await
+        }
+        RemoteType::Pbs => {
+            // let client = connection::make_pbs_client(remote)?;
+            //
+            // let params = crate::pbs_client::AptUpdateParams {
+            //     notify: Some(false),
+            //     quiet: Some(false),
+            // };
+            // let upid = client.update_apt_database(params).await?;
+            //
+            // crate::api::pbs::new_remote_upid(remote.id.clone(), upid).await
+            // TODO: task infrastructure for PBS not finished yet, uncomment once
+            // this is done.
+            bail!("PBS is not supported yet");
         }
-        RemoteType::Pbs => bail!("PBS is not supported yet"),
     }
 }
 
@@ -81,7 +93,14 @@ pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Resul
                 .await
                 .map_err(Into::into)
         }
-        RemoteType::Pbs => bail!("PBS is not supported yet"),
+        RemoteType::Pbs => {
+            let client = connection::make_pbs_client(&remote)?;
+
+            client
+                .get_package_changelog(package, None)
+                .await
+                .map_err(Into::into)
+        }
     }
 }
 
@@ -249,7 +268,15 @@ async fn fetch_available_updates(
                 updates,
             })
         }
-        RemoteType::Pbs => bail!("PBS is not supported yet"),
+        RemoteType::Pbs => {
+            let client = connection::make_pbs_client(&remote)?;
+            let updates = client.list_available_updates().await?;
+
+            Ok(NodeUpdateInfo {
+                last_refresh: proxmox_time::epoch_i64(),
+                updates,
+            })
+        }
     }
 }
 
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 10/13] api: add APT endpoints for PBS remotes
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (8 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 09/13] remote updates: add support for PBS remotes Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 11/13] ui: add remote update view Lukas Wagner
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

Since the endpoints are exactly identical as the PVE ones, we move the
implementation to a shared module and use the same for both.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 server/src/api/pbs/mod.rs        |   4 ++
 server/src/api/pbs/node.rs       |   9 +++
 server/src/api/pve/apt.rs        | 119 -------------------------------
 server/src/api/pve/mod.rs        |   1 -
 server/src/api/pve/node.rs       |   2 +-
 server/src/api/remote_updates.rs | 118 +++++++++++++++++++++++++++++-
 6 files changed, 130 insertions(+), 123 deletions(-)
 create mode 100644 server/src/api/pbs/node.rs
 delete mode 100644 server/src/api/pve/apt.rs

diff --git a/server/src/api/pbs/mod.rs b/server/src/api/pbs/mod.rs
index 56192c01..a37fadb5 100644
--- a/server/src/api/pbs/mod.rs
+++ b/server/src/api/pbs/mod.rs
@@ -20,6 +20,7 @@ use crate::{
 
 use crate::remote_tasks;
 
+mod node;
 mod rrddata;
 
 pub const ROUTER: Router = Router::new()
@@ -41,8 +42,11 @@ pub const MAIN_ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(REMOTE_SUBDIRS))
     .subdirs(REMOTE_SUBDIRS);
 
+const NODES_ROUTER: Router = Router::new().match_all("node", &node::ROUTER);
+
 #[sortable]
 const REMOTE_SUBDIRS: SubdirMap = &sorted!([
+    ("nodes", &NODES_ROUTER),
     ("status", &Router::new().get(&API_METHOD_GET_STATUS)),
     ("rrddata", &rrddata::PBS_NODE_RRD_ROUTER),
     ("datastore", &DATASTORE_ROUTER)
diff --git a/server/src/api/pbs/node.rs b/server/src/api/pbs/node.rs
new file mode 100644
index 00000000..286e1a11
--- /dev/null
+++ b/server/src/api/pbs/node.rs
@@ -0,0 +1,9 @@
+use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
+use proxmox_sortable_macro::sortable;
+
+pub const ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(SUBDIRS))
+    .subdirs(SUBDIRS);
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([("apt", &crate::api::remote_updates::APT_ROUTER),]);
diff --git a/server/src/api/pve/apt.rs b/server/src/api/pve/apt.rs
deleted file mode 100644
index f5027fb8..00000000
--- a/server/src/api/pve/apt.rs
+++ /dev/null
@@ -1,119 +0,0 @@
-use anyhow::Error;
-
-use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
-use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
-use proxmox_schema::api;
-use proxmox_schema::api_types::NODE_SCHEMA;
-
-use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteUpid, PRIV_RESOURCE_MODIFY};
-
-use crate::{api::remotes::get_remote, remote_updates};
-
-#[api(
-    input: {
-        properties: {
-            remote: {
-                schema: REMOTE_ID_SCHEMA,
-            },
-            node: {
-                schema: NODE_SCHEMA,
-            },
-        },
-    },
-    returns: {
-        description: "A list of packages with available updates.",
-        type: Array,
-        items: {
-            type: APTUpdateInfo
-        },
-    },
-    access: {
-        permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
-    },
-)]
-/// List available APT updates for a remote PVE node.
-async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
-    let (config, _digest) = pdm_config::remotes::config()?;
-    let remote = get_remote(&config, &remote)?;
-
-    let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
-
-    Ok(updates)
-}
-
-#[api(
-    input: {
-        properties: {
-            remote: {
-                schema: REMOTE_ID_SCHEMA,
-            },
-            node: {
-                schema: NODE_SCHEMA,
-            },
-        },
-    },
-    access: {
-        permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
-    },
-)]
-/// Update the APT database of a remote PVE node.
-pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
-    let (config, _digest) = pdm_config::remotes::config()?;
-    let remote = get_remote(&config, &remote)?;
-
-    let upid = remote_updates::update_apt_database(remote, &node).await?;
-
-    Ok(upid)
-}
-
-#[api(
-    input: {
-        properties: {
-            remote: {
-                schema: REMOTE_ID_SCHEMA,
-            },
-            node: {
-                schema: NODE_SCHEMA,
-            },
-            options: {
-                type: APTGetChangelogOptions,
-                flatten: true,
-            },
-        },
-    },
-    returns: {
-        description: "The Package changelog.",
-        type: String,
-    },
-    access: {
-        permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
-    },
-)]
-/// Retrieve the changelog of the specified package for a remote PVE node.
-async fn apt_get_changelog(
-    remote: String,
-    node: String,
-    options: APTGetChangelogOptions,
-) -> Result<String, Error> {
-    let (config, _digest) = pdm_config::remotes::config()?;
-    let remote = get_remote(&config, &remote)?;
-
-    remote_updates::get_changelog(remote.clone(), &node, options.name).await
-}
-
-const SUBDIRS: SubdirMap = &[
-    (
-        "changelog",
-        &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
-    ),
-    (
-        "update",
-        &Router::new()
-            .get(&API_METHOD_APT_UPDATE_AVAILABLE)
-            .post(&API_METHOD_APT_UPDATE_DATABASE),
-    ),
-];
-
-pub const ROUTER: Router = Router::new()
-    .get(&list_subdirs_api_method!(SUBDIRS))
-    .subdirs(SUBDIRS);
diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
index a94ab8d0..fd4ea542 100644
--- a/server/src/api/pve/mod.rs
+++ b/server/src/api/pve/mod.rs
@@ -33,7 +33,6 @@ use crate::connection::PveClient;
 use crate::connection::{self, probe_tls_connection};
 use crate::remote_tasks;
 
-mod apt;
 mod lxc;
 mod node;
 mod qemu;
diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
index 1924e252..301c0b19 100644
--- a/server/src/api/pve/node.rs
+++ b/server/src/api/pve/node.rs
@@ -15,7 +15,7 @@ pub const ROUTER: Router = Router::new()
 
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
-    ("apt", &super::apt::ROUTER),
+    ("apt", &crate::api::remote_updates::APT_ROUTER),
     ("rrddata", &super::rrddata::NODE_RRD_ROUTER),
     ("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
     ("storage", &STORAGE_ROUTER),
diff --git a/server/src/api/remote_updates.rs b/server/src/api/remote_updates.rs
index 1feef280..c1309432 100644
--- a/server/src/api/remote_updates.rs
+++ b/server/src/api/remote_updates.rs
@@ -2,10 +2,13 @@
 
 use anyhow::Error;
 
-use pdm_api_types::remote_updates::UpdateSummary;
 use pdm_api_types::remotes::Remote;
-use pdm_api_types::{PRIV_RESOURCE_MODIFY, UPID};
+use pdm_api_types::{
+    remote_updates::UpdateSummary, remotes::REMOTE_ID_SCHEMA, RemoteUpid, NODE_SCHEMA,
+    PRIV_RESOURCE_MODIFY, UPID,
+};
 use proxmox_access_control::CachedUserInfo;
+use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
 use proxmox_rest_server::WorkerTask;
 use proxmox_router::{
     http_bail, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
@@ -15,6 +18,8 @@ use proxmox_sortable_macro::sortable;
 
 use crate::remote_updates;
 
+use super::remotes::get_remote;
+
 pub const ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(SUBDIRS))
     .subdirs(SUBDIRS);
@@ -106,3 +111,112 @@ pub fn refresh_remote_update_summaries(rpcenv: &mut dyn RpcEnvironment) -> Resul
 
     upid_str.parse()
 }
+
+#[api(
+    input: {
+        properties: {
+            remote: {
+                schema: REMOTE_ID_SCHEMA,
+            },
+            node: {
+                schema: NODE_SCHEMA,
+            },
+        },
+    },
+    returns: {
+        description: "A list of packages with available updates.",
+        type: Array,
+        items: {
+            type: APTUpdateInfo
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+    },
+)]
+/// List available APT updates for a remote PVE node.
+async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
+    let (config, _digest) = pdm_config::remotes::config()?;
+    let remote = get_remote(&config, &remote)?;
+
+    let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
+
+    Ok(updates)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: {
+                schema: REMOTE_ID_SCHEMA,
+            },
+            node: {
+                schema: NODE_SCHEMA,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+    },
+)]
+/// Update the APT database of a remote PVE node.
+pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
+    let (config, _digest) = pdm_config::remotes::config()?;
+    let remote = get_remote(&config, &remote)?;
+
+    let upid = remote_updates::update_apt_database(remote, &node).await?;
+
+    Ok(upid)
+}
+
+#[api(
+    input: {
+        properties: {
+            remote: {
+                schema: REMOTE_ID_SCHEMA,
+            },
+            node: {
+                schema: NODE_SCHEMA,
+            },
+            options: {
+                type: APTGetChangelogOptions,
+                flatten: true,
+            },
+        },
+    },
+    returns: {
+        description: "The Package changelog.",
+        type: String,
+    },
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+    },
+)]
+/// Retrieve the changelog of the specified package for a remote PVE node.
+async fn apt_get_changelog(
+    remote: String,
+    node: String,
+    options: APTGetChangelogOptions,
+) -> Result<String, Error> {
+    let (config, _digest) = pdm_config::remotes::config()?;
+    let remote = get_remote(&config, &remote)?;
+
+    remote_updates::get_changelog(remote.clone(), &node, options.name).await
+}
+
+const APT_SUBDIRS: SubdirMap = &[
+    (
+        "changelog",
+        &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
+    ),
+    (
+        "update",
+        &Router::new()
+            .get(&API_METHOD_APT_UPDATE_AVAILABLE)
+            .post(&API_METHOD_APT_UPDATE_DATABASE),
+    ),
+];
+
+pub const APT_ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(APT_SUBDIRS))
+    .subdirs(APT_SUBDIRS);
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 11/13] ui: add remote update view
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (9 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 10/13] api: add APT endpoints " Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-21 19:18   ` Thomas Lamprecht
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 12/13] ui: show new remote update view in the 'Remotes' section Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 13/13] remote updates: avoid unnecessary clone Lukas Wagner
  12 siblings, 1 reply; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

This commit adds a new view for showing a global overview about
available updates on managed remotes. The view is split in the middle.
On the left side, we display a tree view showing all remotes and nodes,
on the right side we show a list of available updates for any node
selected in the tree.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---

Notes:
    Changes since v1:
     - made RemoteUpdateTreeMsg and UpdateTreeComp private
     - format!(...) var inlining
     - remove unneeded borrows
     - use gloo_utils::window()

 ui/src/remotes/mod.rs     |   3 +
 ui/src/remotes/updates.rs | 530 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 533 insertions(+)
 create mode 100644 ui/src/remotes/updates.rs

diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
index 83b3331b..cce21563 100644
--- a/ui/src/remotes/mod.rs
+++ b/ui/src/remotes/mod.rs
@@ -24,6 +24,9 @@ pub use config::{create_remote, RemoteConfigPanel};
 mod tasks;
 pub use tasks::RemoteTaskList;
 
+mod updates;
+pub use updates::UpdateTree;
+
 use yew::{function_component, Html};
 
 use pwt::prelude::*;
diff --git a/ui/src/remotes/updates.rs b/ui/src/remotes/updates.rs
new file mode 100644
index 00000000..6a7ecfb4
--- /dev/null
+++ b/ui/src/remotes/updates.rs
@@ -0,0 +1,530 @@
+use std::cmp::Ordering;
+use std::ops::Deref;
+use std::pin::Pin;
+use std::rc::Rc;
+
+use futures::Future;
+use yew::virtual_dom::{Key, VComp, VNode};
+use yew::{html, Html, Properties};
+
+use pdm_api_types::remote_updates::{
+    NodeUpdateStatus, NodeUpdateSummary, RemoteUpdateStatus, UpdateSummary,
+};
+use pdm_api_types::remotes::RemoteType;
+use pwt::css::{AlignItems, FlexFit, TextAlign};
+use pwt::widget::data_table::{DataTableCellRenderArgs, DataTableCellRenderer};
+
+use proxmox_yew_comp::{
+    AptPackageManager, LoadableComponent, LoadableComponentContext, LoadableComponentMaster,
+};
+use pwt::props::{CssBorderBuilder, CssMarginBuilder, CssPaddingBuilder, WidgetStyleBuilder};
+use pwt::widget::{Button, Container, Panel, Tooltip};
+use pwt::{
+    css,
+    css::FontColor,
+    props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder},
+    state::{Selection, SlabTree, TreeStore},
+    tr,
+    widget::{
+        data_table::{DataTable, DataTableColumn, DataTableHeader},
+        Column, Fa, Row,
+    },
+};
+
+use crate::{get_deep_url, get_deep_url_low_level, pdm_client};
+
+#[derive(PartialEq, Properties)]
+pub struct UpdateTree {}
+
+impl UpdateTree {
+    pub fn new() -> Self {
+        yew::props!(Self {})
+    }
+}
+
+impl From<UpdateTree> for VNode {
+    fn from(value: UpdateTree) -> Self {
+        let comp = VComp::new::<LoadableComponentMaster<UpdateTreeComponent>>(Rc::new(value), None);
+        VNode::from(comp)
+    }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct RemoteEntry {
+    remote: String,
+    ty: RemoteType,
+    number_of_failed_nodes: u32,
+    number_of_nodes: u32,
+    number_of_updatable_nodes: u32,
+    poll_status: RemoteUpdateStatus,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct NodeEntry {
+    remote: String,
+    node: String,
+    ty: RemoteType,
+    summary: NodeUpdateSummary,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+enum UpdateTreeEntry {
+    Root,
+    Remote(RemoteEntry),
+    Node(NodeEntry),
+}
+
+impl UpdateTreeEntry {
+    fn name(&self) -> &str {
+        match &self {
+            Self::Root => "",
+            Self::Remote(data) => &data.remote,
+            Self::Node(data) => &data.node,
+        }
+    }
+}
+
+impl ExtractPrimaryKey for UpdateTreeEntry {
+    fn extract_key(&self) -> yew::virtual_dom::Key {
+        Key::from(match self {
+            UpdateTreeEntry::Root => "/".to_string(),
+            UpdateTreeEntry::Remote(data) => format!("/{}", data.remote),
+            UpdateTreeEntry::Node(data) => format!("/{}/{}", data.remote, data.node),
+        })
+    }
+}
+
+enum RemoteUpdateTreeMsg {
+    LoadFinished(UpdateSummary),
+    KeySelected(Option<Key>),
+    RefreshAll,
+}
+
+struct UpdateTreeComponent {
+    store: TreeStore<UpdateTreeEntry>,
+    selection: Selection,
+    selected_entry: Option<UpdateTreeEntry>,
+}
+
+fn default_sorter(a: &UpdateTreeEntry, b: &UpdateTreeEntry) -> Ordering {
+    a.name().cmp(b.name())
+}
+
+impl UpdateTreeComponent {
+    fn columns(
+        _ctx: &LoadableComponentContext<Self>,
+        store: TreeStore<UpdateTreeEntry>,
+    ) -> Rc<Vec<DataTableHeader<UpdateTreeEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("Name"))
+                .tree_column(store)
+                .width("200px")
+                .render(|entry: &UpdateTreeEntry| {
+                    let icon = match entry {
+                        UpdateTreeEntry::Remote(_) => Some("server"),
+                        UpdateTreeEntry::Node(_) => Some("building"),
+                        _ => None,
+                    };
+
+                    Row::new()
+                        .class(css::AlignItems::Baseline)
+                        .gap(2)
+                        .with_optional_child(icon.map(|icon| Fa::new(icon)))
+                        .with_child(entry.name())
+                        .into()
+                })
+                .sorter(default_sorter)
+                .into(),
+            DataTableColumn::new(tr!("Status"))
+                .render_cell(DataTableCellRenderer::new(
+                    move |args: &mut DataTableCellRenderArgs<UpdateTreeEntry>| {
+                        let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+
+                        match args.record() {
+                            UpdateTreeEntry::Remote(remote_info) => match remote_info.poll_status {
+                                RemoteUpdateStatus::Unknown => {
+                                    row = row.with_child(render_remote_status_icon(
+                                        RemoteUpdateStatus::Unknown,
+                                    ));
+                                }
+                                RemoteUpdateStatus::Success => {
+                                    if !args.is_expanded() {
+                                        let up_to_date_nodes = remote_info.number_of_nodes
+                                            - remote_info.number_of_updatable_nodes
+                                            - remote_info.number_of_failed_nodes;
+
+                                        if up_to_date_nodes > 0 {
+                                            row = row.with_child(render_remote_summary_counter(
+                                                up_to_date_nodes,
+                                                RemoteSummaryIcon::UpToDate,
+                                            ));
+                                        }
+
+                                        if remote_info.number_of_updatable_nodes > 0 {
+                                            row = row.with_child(render_remote_summary_counter(
+                                                remote_info.number_of_updatable_nodes,
+                                                RemoteSummaryIcon::Updatable,
+                                            ));
+                                        }
+
+                                        if remote_info.number_of_failed_nodes > 0 {
+                                            row = row.with_child(render_remote_summary_counter(
+                                                remote_info.number_of_failed_nodes,
+                                                RemoteSummaryIcon::Error,
+                                            ));
+                                        }
+                                    }
+                                }
+                                RemoteUpdateStatus::Error => {
+                                    row = row.with_child(render_remote_status_icon(
+                                        RemoteUpdateStatus::Error,
+                                    ));
+                                }
+                            },
+                            UpdateTreeEntry::Node(info) => {
+                                if info.summary.status == NodeUpdateStatus::Error {
+                                    row = row.with_child(
+                                        Fa::new("times-circle").class(FontColor::Error),
+                                    );
+                                    row = row.with_child(tr!("Could not get update info"));
+                                } else if info.summary.number_of_updates > 0 {
+                                    row = row
+                                        .with_child(Fa::new("refresh").class(FontColor::Primary));
+                                    row = row.with_child(tr!(
+                                        "{0} updates are available",
+                                        info.summary.number_of_updates
+                                    ));
+                                } else {
+                                    row =
+                                        row.with_child(Fa::new("check").class(FontColor::Success));
+                                    row = row.with_child(tr!("Up-to-date"));
+                                }
+                            }
+                            _ => {}
+                        }
+
+                        row.into()
+                    },
+                ))
+                .into(),
+        ])
+    }
+}
+
+fn build_store_from_response(update_summary: UpdateSummary) -> SlabTree<UpdateTreeEntry> {
+    let mut tree = SlabTree::new();
+
+    let mut root = tree.set_root(UpdateTreeEntry::Root);
+    root.set_expanded(true);
+
+    for (remote_name, remote_summary) in update_summary.remotes.deref() {
+        let mut remote_entry = root.append(UpdateTreeEntry::Remote(RemoteEntry {
+            remote: remote_name.clone(),
+            ty: remote_summary.remote_type,
+            number_of_nodes: 0,
+            number_of_updatable_nodes: 0,
+            number_of_failed_nodes: 0,
+            poll_status: remote_summary.status.clone(),
+        }));
+        remote_entry.set_expanded(false);
+
+        let number_of_nodes = remote_summary.nodes.len();
+        let mut number_of_updatable_nodes = 0;
+        let mut number_of_failed_nodes = 0;
+
+        for (node_name, node_summary) in remote_summary.nodes.deref() {
+            match node_summary.status {
+                NodeUpdateStatus::Success => {
+                    if node_summary.number_of_updates > 0 {
+                        number_of_updatable_nodes += 1;
+                    }
+                }
+                NodeUpdateStatus::Error => {
+                    number_of_failed_nodes += 1;
+                }
+            }
+
+            remote_entry.append(UpdateTreeEntry::Node(NodeEntry {
+                remote: remote_name.clone(),
+                node: node_name.clone(),
+                ty: remote_summary.remote_type,
+                summary: node_summary.clone(),
+            }));
+        }
+
+        if let UpdateTreeEntry::Remote(info) = remote_entry.record_mut() {
+            info.number_of_updatable_nodes = number_of_updatable_nodes;
+            info.number_of_nodes = number_of_nodes as u32;
+            info.number_of_failed_nodes = number_of_failed_nodes as u32;
+        }
+    }
+
+    tree
+}
+
+impl LoadableComponent for UpdateTreeComponent {
+    type Properties = UpdateTree;
+    type Message = RemoteUpdateTreeMsg;
+    type ViewState = ();
+
+    fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+        let link = ctx.link();
+
+        let store = TreeStore::new().view_root(false);
+        store.set_sorter(default_sorter);
+
+        link.repeated_load(5000);
+
+        let selection = Selection::new().on_select(link.callback(|selection: Selection| {
+            RemoteUpdateTreeMsg::KeySelected(selection.selected_key())
+        }));
+
+        Self {
+            store: store.clone(),
+            selection,
+            selected_entry: None,
+        }
+    }
+
+    fn load(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+    ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+        let link = ctx.link().clone();
+
+        Box::pin(async move {
+            let client = pdm_client();
+
+            let updates = client.remote_update_summary().await?;
+            link.send_message(Self::Message::LoadFinished(updates));
+
+            Ok(())
+        })
+    }
+
+    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Self::Message::LoadFinished(updates) => {
+                let data = build_store_from_response(updates);
+                self.store.write().update_root_tree(data);
+                self.store.set_sorter(default_sorter);
+
+                return true;
+            }
+            Self::Message::KeySelected(key) => {
+                if let Some(key) = key {
+                    let read_guard = self.store.read();
+                    let node_ref = read_guard.lookup_node(&key).unwrap();
+                    let record = node_ref.record();
+
+                    self.selected_entry = Some(record.clone());
+
+                    return true;
+                }
+            }
+            Self::Message::RefreshAll => {
+                let link = ctx.link();
+
+                link.clone().spawn(async move {
+                    let client = pdm_client();
+
+                    match client.refresh_remote_update_summary().await {
+                        Ok(upid) => {
+                            link.show_task_progres(upid.to_string());
+                        }
+                        Err(err) => {
+                            link.show_error(tr!("Could not refresh update status."), err, false);
+                        }
+                    }
+                });
+            }
+        }
+
+        false
+    }
+
+    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> yew::Html {
+        Container::new()
+            .class("pwt-content-spacer")
+            .class(FlexFit)
+            .class("pwt-flex-direction-row")
+            .with_child(self.render_update_tree_panel(ctx))
+            .with_child(self.render_update_list_panel(ctx))
+            .into()
+    }
+}
+
+impl UpdateTreeComponent {
+    fn render_update_tree_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
+        let table = DataTable::new(Self::columns(ctx, self.store.clone()), self.store.clone())
+            .selection(self.selection.clone())
+            .striped(false)
+            .borderless(true)
+            .show_header(false)
+            .class(css::FlexFit);
+
+        let refresh_all_button = Button::new(tr!("Refresh all")).on_activate({
+            let link = ctx.link().clone();
+            move |_| {
+                link.send_message(RemoteUpdateTreeMsg::RefreshAll);
+            }
+        });
+
+        let title: Html = Row::new()
+            .gap(2)
+            .class(AlignItems::Baseline)
+            .with_child(Fa::new("refresh"))
+            .with_child(tr!("Remote System Updates"))
+            .into();
+
+        Panel::new()
+            .min_width(500)
+            .title(title)
+            .with_tool(refresh_all_button)
+            .style("flex", "1 1 0")
+            .class(FlexFit)
+            .border(true)
+            .with_child(table)
+    }
+
+    fn render_update_list_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
+        let title: Html = Row::new()
+            .gap(2)
+            .class(AlignItems::Baseline)
+            .with_child(Fa::new("list"))
+            .with_child(tr!("Update List"))
+            .into();
+
+        match &self.selected_entry {
+            // (Some(remote), Some(remote_type), Some(node)) => {
+            Some(UpdateTreeEntry::Node(NodeEntry {
+                remote, node, ty, ..
+            })) => {
+                let base_url = format!("/{ty}/remotes/{remote}/nodes/{node}/apt",);
+                let task_base_url = format!("/{ty}/remotes/{remote}/tasks");
+
+                let apt = AptPackageManager::new()
+                    .base_url(base_url)
+                    .task_base_url(task_base_url)
+                    .enable_upgrade(true)
+                    .on_upgrade({
+                        let remote = remote.clone();
+                        let link = ctx.link().clone();
+                        let remote = remote.clone();
+                        let node = node.clone();
+                        let ty = *ty;
+
+                        move |_| match ty {
+                            RemoteType::Pve => {
+                                let id = format!("node/{node}::apt");
+                                if let Some(url) = get_deep_url(link.yew_link(), &remote, None, &id)
+                                {
+                                    let _ = gloo_utils::window().open_with_url(&url.href());
+                                }
+                            }
+                            RemoteType::Pbs => {
+                                let hash = "#pbsServerAdministration:updates";
+                                if let Some(url) =
+                                    get_deep_url_low_level(link.yew_link(), &remote, None, &hash)
+                                {
+                                    let _ = gloo_utils::window().open_with_url(&url.href());
+                                }
+                            }
+                        }
+                    });
+
+                Panel::new()
+                    .class(FlexFit)
+                    .title(title)
+                    .border(true)
+                    .min_width(500)
+                    .with_child(apt)
+                    .style("flex", "1 1 0")
+            }
+            _ => {
+                let header = tr!("No node selected");
+                let msg = tr!("Select a node to show available updates.");
+
+                let select_node_msg = Column::new()
+                    .class(FlexFit)
+                    .padding(2)
+                    .class(AlignItems::Center)
+                    .class(TextAlign::Center)
+                    .with_child(html! {<h1 class="pwt-font-headline-medium">{header}</h1>})
+                    .with_child(Container::new().with_child(msg));
+
+                Panel::new()
+                    .class(FlexFit)
+                    .title(title)
+                    .border(true)
+                    .min_width(500)
+                    .with_child(select_node_msg)
+                    .style("flex", "1 1 0")
+            }
+        }
+    }
+}
+
+enum RemoteSummaryIcon {
+    UpToDate,
+    Updatable,
+    Error,
+}
+
+fn render_remote_summary_counter(count: u32, task_class: RemoteSummaryIcon) -> Html {
+    let (icon_class, icon_scheme, state_text) = match task_class {
+        RemoteSummaryIcon::UpToDate => (
+            "check",
+            FontColor::Success,
+            tr!("One node is up-to-date." | "{n} nodes are up-to-date." % count),
+        ),
+        RemoteSummaryIcon::Error => (
+            "times-circle",
+            FontColor::Error,
+            tr!("Failed to retrieve update info for one node."
+                | "Failed to retrieve update info for {n} nodes." % count),
+        ),
+        RemoteSummaryIcon::Updatable => (
+            "refresh",
+            FontColor::Primary,
+            tr!("One node has updates available." | "{n} nodes have updates available." % count),
+        ),
+    };
+
+    let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
+
+    Tooltip::new(
+        Container::from_tag("span")
+            .with_child(icon)
+            .with_child(count)
+            .margin_end(5),
+    )
+    .tip(state_text)
+    .into()
+}
+
+fn render_remote_status_icon(task_class: RemoteUpdateStatus) -> Html {
+    let (icon_class, icon_scheme, state_text) = match task_class {
+        RemoteUpdateStatus::Success => (
+            "check",
+            FontColor::Success,
+            tr!("All nodes of this remote are up-to-date."),
+        ),
+        RemoteUpdateStatus::Error => (
+            "times-circle",
+            FontColor::Error,
+            tr!("Could not retrieve update info for remote."),
+        ),
+        RemoteUpdateStatus::Unknown => (
+            "question-circle-o",
+            FontColor::Primary,
+            tr!("The update status is not known."),
+        ),
+    };
+
+    let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
+
+    Tooltip::new(Container::from_tag("span").with_child(icon).margin_end(5))
+        .tip(state_text)
+        .into()
+}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 12/13] ui: show new remote update view in the 'Remotes' section
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (10 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 11/13] ui: add remote update view Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 13/13] remote updates: avoid unnecessary clone Lukas Wagner
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Shannon Sterz <s.sterz@proxmox.com>
---
 ui/src/remotes/mod.rs | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
index cce21563..5e06b2ce 100644
--- a/ui/src/remotes/mod.rs
+++ b/ui/src/remotes/mod.rs
@@ -56,6 +56,13 @@ pub fn system_configuration() -> Html {
                 .label(tr!("Tasks"))
                 .icon_class("fa fa-book"),
             |_| RemoteTaskList::new().into(),
+        )
+        .with_item_builder(
+            TabBarItem::new()
+                .key("updates")
+                .label(tr!("Updates"))
+                .icon_class("fa fa-refresh"),
+            |_| UpdateTree::new().into(),
         );
 
     NavigationContainer::new().with_child(panel).into()
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] [PATCH datacenter-manager v2 13/13] remote updates: avoid unnecessary clone
  2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
                   ` (11 preceding siblings ...)
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 12/13] ui: show new remote update view in the 'Remotes' section Lukas Wagner
@ 2025-10-17 12:10 ` Lukas Wagner
  12 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-17 12:10 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    New in v2

 server/src/api/remote_updates.rs | 2 +-
 server/src/remote_updates.rs     | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/server/src/api/remote_updates.rs b/server/src/api/remote_updates.rs
index c1309432..bea2d864 100644
--- a/server/src/api/remote_updates.rs
+++ b/server/src/api/remote_updates.rs
@@ -201,7 +201,7 @@ async fn apt_get_changelog(
     let (config, _digest) = pdm_config::remotes::config()?;
     let remote = get_remote(&config, &remote)?;
 
-    remote_updates::get_changelog(remote.clone(), &node, options.name).await
+    remote_updates::get_changelog(remote, &node, options.name).await
 }
 
 const APT_SUBDIRS: SubdirMap = &[
diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
index 4c70495b..87c6afd9 100644
--- a/server/src/remote_updates.rs
+++ b/server/src/remote_updates.rs
@@ -83,10 +83,10 @@ pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUp
 }
 
 /// Get the changelog for a given package.
-pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Result<String, Error> {
+pub async fn get_changelog(remote: &Remote, node: &str, package: String) -> Result<String, Error> {
     match remote.ty {
         RemoteType::Pve => {
-            let client = connection::make_pve_client(&remote)?;
+            let client = connection::make_pve_client(remote)?;
 
             client
                 .get_package_changelog(node, package, None)
@@ -94,7 +94,7 @@ pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Resul
                 .map_err(Into::into)
         }
         RemoteType::Pbs => {
-            let client = connection::make_pbs_client(&remote)?;
+            let client = connection::make_pbs_client(remote)?;
 
             client
                 .get_package_changelog(package, None)
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* Re: [pdm-devel] [PATCH datacenter-manager v2 11/13] ui: add remote update view
  2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 11/13] ui: add remote update view Lukas Wagner
@ 2025-10-21 19:18   ` Thomas Lamprecht
  2025-10-22 10:22     ` Lukas Wagner
  0 siblings, 1 reply; 17+ messages in thread
From: Thomas Lamprecht @ 2025-10-21 19:18 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Lukas Wagner

Nice work overall, some things that I noticed inline.

Am 17.10.25 um 14:10 schrieb Lukas Wagner:
> +fn render_remote_summary_counter(count: u32, task_class: RemoteSummaryIcon) -> Html {
> +    let (icon_class, icon_scheme, state_text) = match task_class {
> +        RemoteSummaryIcon::UpToDate => (
> +            "check",
> +            FontColor::Success,
> +            tr!("One node is up-to-date." | "{n} nodes are up-to-date." % count),
> +        ),
> +        RemoteSummaryIcon::Error => (
> +            "times-circle",
> +            FontColor::Error,
> +            tr!("Failed to retrieve update info for one node."
> +                | "Failed to retrieve update info for {n} nodes." % count),
> +        ),
> +        RemoteSummaryIcon::Updatable => (
> +            "refresh",
> +            FontColor::Primary,
> +            tr!("One node has updates available." | "{n} nodes have updates available." % count),
> +        ),
> +    };
> +
> +    let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
> +
> +    Tooltip::new(
> +        Container::from_tag("span")
> +            .with_child(icon)
> +            .with_child(count)
> +            .margin_end(5),
> +    )
> +    .tip(state_text)

I found the standalone numbers a bit confusing, i.e. interpreted them as "X updates
available" at first. As we got the horizontal space, what about moving the state_text
out of a tooltip and make it the always visible text?

btw. could be nice to show single-node remotes (PBS, single-node PVEs and potentially
PMG in the future) directly at the top level, i.e. without a nesting level indirection.
That would save a bit vertical space and avoid clicks.

Some other things that might not belong to this reply but I noticed:
- do we have the last apt update time available? could be nice to show that as column,
  e.g. colored as warning if it's older than a day or so (but can be added anytime so
  definitively not a blocker now).

- Might be nicer to add the horizontal scrolling to the inner views, as with 1440x900
  there are already columns cut-off in the Update List view on the right, and that
  resolution is definitively one that should still be usable (but doesn't have to look
  great).

- Repo state would be really good to see here, as else one might get a false sense
  of security/safety if all is green checkmarks, but that then being the result of
  bad/no repos configured over the system being actually fully up-to-date.

Besides the unlabeled number these can all be follow ups (if at all), so I'm fine with
applying this as is, but you might have a better gut feeling if it's fine to do follow-ups
over a v3, so just tell me what you prefer.

> +    .into()
> +}

...

> +    fn render_update_list_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
> +        let title: Html = Row::new()
> +            .gap(2)
> +            .class(AlignItems::Baseline)
> +            .with_child(Fa::new("list"))
> +            .with_child(tr!("Update List"))
> +            .into();

Might be nice to see the selected nodename in the title, especially with many remotes/nodes
and the right list having been scrolled so that the selected one is out of view.




_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* [pdm-devel] applied: [PATCH datacenter-manager v2 01/13] metric collection task: tests: add missing parameter for cluster_metric_export
  2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 01/13] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
@ 2025-10-21 19:24   ` Thomas Lamprecht
  0 siblings, 0 replies; 17+ messages in thread
From: Thomas Lamprecht @ 2025-10-21 19:24 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Lukas Wagner

Am 17.10.25 um 14:10 schrieb Lukas Wagner:
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
> Tested-by: Shannon Sterz <s.sterz@proxmox.com>
> ---
>  server/src/metric_collection/collection_task.rs | 1 +
>  1 file changed, 1 insertion(+)
> 
>

applied this one already, thanks!


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

* Re: [pdm-devel] [PATCH datacenter-manager v2 11/13] ui: add remote update view
  2025-10-21 19:18   ` Thomas Lamprecht
@ 2025-10-22 10:22     ` Lukas Wagner
  0 siblings, 0 replies; 17+ messages in thread
From: Lukas Wagner @ 2025-10-22 10:22 UTC (permalink / raw)
  To: Thomas Lamprecht,
	Proxmox Datacenter Manager development discussion, Lukas Wagner

On Tue Oct 21, 2025 at 9:18 PM CEST, Thomas Lamprecht wrote:
> Nice work overall, some things that I noticed inline.
>
> Am 17.10.25 um 14:10 schrieb Lukas Wagner:
>> +fn render_remote_summary_counter(count: u32, task_class: RemoteSummaryIcon) -> Html {
>> +    let (icon_class, icon_scheme, state_text) = match task_class {
>> +        RemoteSummaryIcon::UpToDate => (
>> +            "check",
>> +            FontColor::Success,
>> +            tr!("One node is up-to-date." | "{n} nodes are up-to-date." % count),
>> +        ),
>> +        RemoteSummaryIcon::Error => (
>> +            "times-circle",
>> +            FontColor::Error,
>> +            tr!("Failed to retrieve update info for one node."
>> +                | "Failed to retrieve update info for {n} nodes." % count),
>> +        ),
>> +        RemoteSummaryIcon::Updatable => (
>> +            "refresh",
>> +            FontColor::Primary,
>> +            tr!("One node has updates available." | "{n} nodes have updates available." % count),
>> +        ),
>> +    };
>> +
>> +    let icon = Fa::new(icon_class).margin_end(3).class(icon_scheme);
>> +
>> +    Tooltip::new(
>> +        Container::from_tag("span")
>> +            .with_child(icon)
>> +            .with_child(count)
>> +            .margin_end(5),
>> +    )
>> +    .tip(state_text)
>
> I found the standalone numbers a bit confusing, i.e. interpreted them as "X updates
> available" at first. As we got the horizontal space, what about moving the state_text
> out of a tooltip and make it the always visible text?
>

Fair point, I can see how that could be confusing.

The main issue is that a remote as a whole can have multiple states at
the same time, one for each node. For example, a remote with four nodes
could have all of these at the same time:

  - One node is up-to-date
  - Two nodes have updates available
  - One node not reachable
  - (Repo configuration issues - once implemented)

For a collapsed tree item that represents the entire remote, it's pretty
hard to 'summarise' everything into a single line in text form and still
fit the view, and that is why I went with the status icons plus the
number of nodes that have the given status.

After evaluating your suggestion, I discussed the issue with
Maximiliano. One idea that came up was to remove the counter to avoid
any confusion and to just show the icon itself. After all, at a glance,
for the summary it does not matter that much *how many* nodes have
updates, but that *some* nodes have updates available, or in general,
need attention. To get the full picture for a remote, one can always
just expand the item in the tree.

We also discussed whether it would make sense to not show the 'green
checkmark' at all in the summary (when the remote is collapsed) and just
use icons where it makes sense to draw attention from the user (updates
available, some error, warnings about the repo configuration).
This could make it easier to spot actionable remotes quickly.
When the item is expanded, we would still show the green checkmark plus
text for up-to-date nodes.

TL;DR: My next approach would be to drop the counter and also skip the
green checkmark in case everything is up-to-date (for the collapsed
remote item in the tree, the node 'leaves' would stay the same)

What do you think?

> btw. could be nice to show single-node remotes (PBS, single-node PVEs and potentially
> PMG in the future) directly at the top level, i.e. without a nesting level indirection.
> That would save a bit vertical space and avoid clicks.

Yeah, I had a similar idea already, and generally I believe this could
be a good addition to improve UX. That being said, I think with the idea
described above, where we show an icon-only summary for expandable items
in the tree, it could look a bit odd if we show it directly at the top
level, since then you would have icon-only and 'icon + text' tree items
side-by-side, at the same level. I'll have to actually try this out and
see how 'bad' this is, but I'll definitely keep it in mind.

I'd say this would definitely be material for a follow-up patch.

>
> Some other things that might not belong to this reply but I noticed:
> - do we have the last apt update time available? could be nice to show that as column,
>   e.g. colored as warning if it's older than a day or so (but can be added anytime so
>   definitively not a blocker now).

Right now, we save the timestamp of when we successfully polled a list
of updates from a node, but that's purely done on the PDM side - we
don't really know how fresh the apt database on the node is and there is
at the moment no API for this. For automatic refreshes of the cached
update state, we don't actually invoke 'apt update' on the node, but
rely on the daily update task on the node itself.

A nice addition that I already had in mind would be to augment the 
GET /nodes/.../apt/update endpoint in PVE/PBS to also return the timestamp
of the last 'apt update' (or add a new endpoint, if adding it to the
existing one turns out to be hard due to the structure of the response).

>
> - Might be nicer to add the horizontal scrolling to the inner views, as with 1440x900
>   there are already columns cut-off in the Update List view on the right, and that
>   resolution is definitively one that should still be usable (but doesn't have to look
>   great).

For me horizontal scrolling already works for the update list on the
right, but maybe I misunderstand you?

Some idea that I already had was to hide the 'Description' column by
default for the 'half-width' update lists like here and then find some
other way to display the description on demand (e.g. mouse-over, or
change the 'Changelog' button to a 'Details' button, which then shows
the description *and* the changelog in a dialog)
>
> - Repo state would be really good to see here, as else one might get a false sense
>   of security/safety if all is green checkmarks, but that then being the result of
>   bad/no repos configured over the system being actually fully up-to-date.

Noted and already planned for the future, the API and the UI were
already designed with this in mind.

>
> Besides the unlabeled number these can all be follow ups (if at all), so I'm fine with
> applying this as is, but you might have a better gut feeling if it's fine to do follow-ups
> over a v3, so just tell me what you prefer.
>

I'll post a v3 with the icon-only status column so that you can try this
out and give your opinion. v3 also fixes some small bug that I found
while toying around with different ideas for the icon-only status line.

>> +    .into()
>> +}
>
> ...
>
>> +    fn render_update_list_panel(&self, ctx: &LoadableComponentContext<Self>) -> Panel {
>> +        let title: Html = Row::new()
>> +            .gap(2)
>> +            .class(AlignItems::Baseline)
>> +            .with_child(Fa::new("list"))
>> +            .with_child(tr!("Update List"))
>> +            .into();
>
> Might be nice to see the selected nodename in the title, especially with many remotes/nodes
> and the right list having been scrolled so that the selected one is out of view.

Will include this as well in a v3.


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


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

end of thread, other threads:[~2025-10-22 10:22 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-17 12:09 [pdm-devel] [PATCH datacenter-manager v2 00/13] add global remote update view Lukas Wagner
2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 01/13] metric collection task: tests: add missing parameter for cluster_metric_export Lukas Wagner
2025-10-21 19:24   ` [pdm-devel] applied: " Thomas Lamprecht
2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 02/13] pdm-api-types: add types for remote upgrade summary Lukas Wagner
2025-10-17 12:09 ` [pdm-devel] [PATCH datacenter-manager v2 03/13] remote updates: add cache for remote update availability Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 04/13] api: add API for retrieving/refreshing the remote update summary Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 05/13] unprivileged api daemon: tasks: add remote update refresh task Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 06/13] pdm-client: add API methods for remote update summaries Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 07/13] pbs-client: add bindings for APT-related API calls Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 08/13] task cache: use separate functions for tracking PVE and PBS tasks Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 09/13] remote updates: add support for PBS remotes Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 10/13] api: add APT endpoints " Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 11/13] ui: add remote update view Lukas Wagner
2025-10-21 19:18   ` Thomas Lamprecht
2025-10-22 10:22     ` Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 12/13] ui: show new remote update view in the 'Remotes' section Lukas Wagner
2025-10-17 12:10 ` [pdm-devel] [PATCH datacenter-manager v2 13/13] remote updates: avoid unnecessary clone 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