From 34ddae7eb20a27e1fc59b034d6510d54f0a98ef4 Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Sat, 20 Jun 2026 19:26:39 -0500 Subject: [PATCH] fix(proxmox): remove double-unwrap of Proxmox data envelope across all modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handle_response() in client.rs already strips the {"data":...} wrapper before returning to callers. Every proxmox module was calling .get("data") a second time on the already-unwrapped Value, which always returned None and caused all API responses to silently yield empty results or errors. vm.rs had an additional bug: list_vms used POST on cluster/resources (a GET-only endpoint) and dropped VMs with no cpu field via filter_map ? instead of unwrap_or(0.0). Both corrected. Affected modules: vm, ceph, ceph_cluster, certificates, acme, firewall, sdn, ha, apt, updates, updates_ext, tasks, migration, metrics, shell, auth_realm, views, backup — 18 files, 19 functions. 426 Rust tests pass. clippy -D warnings clean. tsc --noEmit clean. --- src-tauri/src/proxmox/acme.rs | 19 +++--- src-tauri/src/proxmox/apt.rs | 78 +++++++++++---------- src-tauri/src/proxmox/auth_realm.rs | 4 +- src-tauri/src/proxmox/backup.rs | 14 ++-- src-tauri/src/proxmox/ceph.rs | 22 +++--- src-tauri/src/proxmox/ceph_cluster.rs | 9 ++- src-tauri/src/proxmox/certificates.rs | 19 +++--- src-tauri/src/proxmox/firewall.rs | 14 ++-- src-tauri/src/proxmox/ha.rs | 8 +-- src-tauri/src/proxmox/metrics.rs | 9 ++- src-tauri/src/proxmox/migration.rs | 14 ++-- src-tauri/src/proxmox/sdn.rs | 12 ++-- src-tauri/src/proxmox/shell.rs | 7 +- src-tauri/src/proxmox/tasks.rs | 18 +++-- src-tauri/src/proxmox/updates.rs | 18 +++-- src-tauri/src/proxmox/updates_ext.rs | 16 ++--- src-tauri/src/proxmox/views.rs | 9 ++- src-tauri/src/proxmox/vm.rs | 97 +++++++++++++-------------- 18 files changed, 178 insertions(+), 209 deletions(-) diff --git a/src-tauri/src/proxmox/acme.rs b/src-tauri/src/proxmox/acme.rs index da588fdc..7e1be46f 100644 --- a/src-tauri/src/proxmox/acme.rs +++ b/src-tauri/src/proxmox/acme.rs @@ -44,7 +44,7 @@ pub async fn list_acme_accounts( .await .map_err(|e| format!("Failed to list ACME accounts: {}", e))?; - if let Some(accounts) = response.get("data").and_then(|d| d.as_array()) { + if let Some(accounts) = response.as_array() { let account_list: Vec = accounts .iter() .filter_map(|account| { @@ -94,7 +94,8 @@ pub async fn register_acme_account( .await .map_err(|e| format!("Failed to register ACME account: {}", e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -117,8 +118,6 @@ pub async fn register_acme_account( status, created_at, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -134,7 +133,7 @@ pub async fn get_acme_challenges( .await .map_err(|e| format!("Failed to get ACME challenges for {}: {}", domain, e))?; - if let Some(challenges) = response.get("data").and_then(|d| d.as_array()) { + if let Some(challenges) = response.as_array() { let challenge_list: Vec = challenges .iter() .filter_map(|challenge| { @@ -191,7 +190,8 @@ pub async fn request_certificate( .await .map_err(|e| format!("Failed to request ACME certificate: {}", e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -230,8 +230,6 @@ pub async fn request_certificate( expires_at, issuer, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -247,7 +245,8 @@ pub async fn get_certificate_details( .await .map_err(|e| format!("Failed to get ACME certificate {}: {}", cert_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -286,8 +285,6 @@ pub async fn get_certificate_details( expires_at, issuer, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } diff --git a/src-tauri/src/proxmox/apt.rs b/src-tauri/src/proxmox/apt.rs index 3405ad60..8c1d93b4 100644 --- a/src-tauri/src/proxmox/apt.rs +++ b/src-tauri/src/proxmox/apt.rs @@ -37,8 +37,7 @@ pub async fn list_apt_updates( .map_err(|e| format!("Failed to list APT updates: {}", e))?; let updates: Vec = response - .get("data") - .and_then(|d| d.as_array()) + .as_array() .map(|arr| { arr.iter() .filter_map(|update| { @@ -97,47 +96,46 @@ pub async fn list_apt_repositories( .await .map_err(|e| format!("Failed to list APT repositories: {}", e))?; - if let Some(repos) = response.get("data").and_then(|d| d.as_array()) { - let repo_list: Vec = repos - .iter() - .filter_map(|repo| { - let id = repo.get("id")?.as_str()?.to_string(); - let url = repo.get("url")?.as_str().unwrap_or("").to_string(); - let distribution = repo - .get("distribution") - .and_then(|d| d.as_str()) - .unwrap_or("") - .to_string(); - let component = repo - .get("component") - .and_then(|c| c.as_str()) - .unwrap_or("") - .to_string(); - let enabled = repo - .get("enabled") - .and_then(|e| e.as_bool()) - .unwrap_or(true); - let type_ = repo - .get("type") - .and_then(|t| t.as_str()) - .unwrap_or("deb") - .to_string(); + let repos = response + .as_array() + .ok_or_else(|| "Invalid response format: expected array".to_string())?; + let repo_list: Vec = repos + .iter() + .filter_map(|repo| { + let id = repo.get("id")?.as_str()?.to_string(); + let url = repo.get("url")?.as_str().unwrap_or("").to_string(); + let distribution = repo + .get("distribution") + .and_then(|d| d.as_str()) + .unwrap_or("") + .to_string(); + let component = repo + .get("component") + .and_then(|c| c.as_str()) + .unwrap_or("") + .to_string(); + let enabled = repo + .get("enabled") + .and_then(|e| e.as_bool()) + .unwrap_or(true); + let type_ = repo + .get("type") + .and_then(|t| t.as_str()) + .unwrap_or("deb") + .to_string(); - Some(APTRepository { - repository_id: id, - url, - distribution, - component, - enabled, - type_, - }) + Some(APTRepository { + repository_id: id, + url, + distribution, + component, + enabled, + type_, }) - .collect(); + }) + .collect(); - Ok(repo_list) - } else { - Err("Invalid response format: missing 'data' field".to_string()) - } + Ok(repo_list) } /// Add APT repository diff --git a/src-tauri/src/proxmox/auth_realm.rs b/src-tauri/src/proxmox/auth_realm.rs index 22a98bb6..e7ab333a 100644 --- a/src-tauri/src/proxmox/auth_realm.rs +++ b/src-tauri/src/proxmox/auth_realm.rs @@ -62,7 +62,7 @@ pub async fn list_auth_realms( .await .map_err(|e| format!("Failed to list authentication realms: {}", e))?; - if let Some(realms) = response.get("data").and_then(|d| d.as_array()) { + if let Some(realms) = response.as_array() { let realm_list: Vec = realms .iter() .filter_map(|realm| { @@ -89,7 +89,7 @@ pub async fn list_auth_realms( Ok(realm_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } diff --git a/src-tauri/src/proxmox/backup.rs b/src-tauri/src/proxmox/backup.rs index 0ca5d433..ac9fa916 100644 --- a/src-tauri/src/proxmox/backup.rs +++ b/src-tauri/src/proxmox/backup.rs @@ -38,7 +38,7 @@ pub async fn list_backup_jobs( .await .map_err(|e| format!("Failed to list backup jobs: {}", e))?; - if let Some(jobs) = response.get("data").and_then(|d| d.as_array()) { + if let Some(jobs) = response.as_array() { let backup_jobs: Vec = jobs .iter() .filter_map(|job| { @@ -64,7 +64,7 @@ pub async fn list_backup_jobs( Ok(backup_jobs) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } @@ -159,7 +159,7 @@ pub async fn list_datastores( .await .map_err(|e| format!("Failed to list datastores: {}", e))?; - if let Some(datastores) = response.get("data").and_then(|d| d.as_array()) { + if let Some(datastores) = response.as_array() { let datastore_list: Vec = datastores .iter() .filter_map(|ds| { @@ -183,7 +183,7 @@ pub async fn list_datastores( Ok(datastore_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } @@ -200,7 +200,7 @@ pub async fn get_datastore_status( .await .map_err(|e| format!("Failed to get datastore status: {}", e))?; - let ds = response.get("data").ok_or("Invalid response format")?; + let ds = &response; Ok(DatastoreInfo { datastore: datastore.to_string(), @@ -229,10 +229,10 @@ pub async fn list_backup_snapshots( .await .map_err(|e| format!("Failed to list backup snapshots: {}", e))?; - if let Some(snapshots) = response.get("data").and_then(|d| d.as_array()) { + if let Some(snapshots) = response.as_array() { Ok(snapshots.to_vec()) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } diff --git a/src-tauri/src/proxmox/ceph.rs b/src-tauri/src/proxmox/ceph.rs index 4bf741d1..ea693829 100644 --- a/src-tauri/src/proxmox/ceph.rs +++ b/src-tauri/src/proxmox/ceph.rs @@ -55,7 +55,7 @@ pub async fn list_pools( .await .map_err(|e| format!("Failed to list Ceph pools: {}", e))?; - if let Some(pools) = response.get("data").and_then(|d| d.as_array()) { + if let Some(pools) = response.as_array() { let pool_list: Vec = pools .iter() .filter_map(|pool| { @@ -87,7 +87,7 @@ pub async fn list_pools( Ok(pool_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -155,7 +155,7 @@ pub async fn list_osds( .await .map_err(|e| format!("Failed to list Ceph OSDs: {}", e))?; - if let Some(osds) = response.get("data").and_then(|d| d.as_array()) { + if let Some(osds) = response.as_array() { let osd_list: Vec = osds .iter() .filter_map(|osd| { @@ -179,7 +179,7 @@ pub async fn list_osds( Ok(osd_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -241,10 +241,10 @@ pub async fn list_mds( .await .map_err(|e| format!("Failed to list Ceph MDS: {}", e))?; - if let Some(mds) = response.get("data").and_then(|d| d.as_array()) { + if let Some(mds) = response.as_array() { Ok(mds.to_vec()) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -287,10 +287,10 @@ pub async fn list_rbd( .await .map_err(|e| format!("Failed to list RBD images in pool {}: {}", pool, e))?; - if let Some(images) = response.get("data").and_then(|d| d.as_array()) { + if let Some(images) = response.as_array() { Ok(images.to_vec()) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -415,7 +415,7 @@ pub async fn list_monitors( .await .map_err(|e| format!("Failed to list Ceph monitors: {}", e))?; - if let Some(mons) = response.get("data").and_then(|d| d.as_array()) { + if let Some(mons) = response.as_array() { let mon_list: Vec = mons .iter() .filter_map(|mon| { @@ -439,7 +439,7 @@ pub async fn list_monitors( Ok(mon_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -479,7 +479,7 @@ pub async fn get_ceph_health( .await .map_err(|e| format!("Failed to get Ceph health: {}", e))?; - let health = response.get("data").ok_or("Invalid response format")?; + let health = &response; let details: Vec = health .get("details") diff --git a/src-tauri/src/proxmox/ceph_cluster.rs b/src-tauri/src/proxmox/ceph_cluster.rs index dd013507..22acc65e 100644 --- a/src-tauri/src/proxmox/ceph_cluster.rs +++ b/src-tauri/src/proxmox/ceph_cluster.rs @@ -45,7 +45,7 @@ pub async fn list_ceph_clusters( .await .map_err(|e| format!("Failed to list Ceph clusters: {}", e))?; - if let Some(clusters) = response.get("data").and_then(|d| d.as_array()) { + if let Some(clusters) = response.as_array() { let cluster_list: Vec = clusters .iter() .filter_map(|cluster| { @@ -150,7 +150,7 @@ pub async fn list_ceph_clusters( Ok(cluster_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -166,7 +166,8 @@ pub async fn get_ceph_cluster_status( .await .map_err(|e| format!("Failed to get Ceph cluster {} status: {}", cluster_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("cluster_id") .and_then(|i| i.as_str()) @@ -195,8 +196,6 @@ pub async fn get_ceph_cluster_status( osd_map, pg_map, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } diff --git a/src-tauri/src/proxmox/certificates.rs b/src-tauri/src/proxmox/certificates.rs index 5db23aad..50f5aca3 100644 --- a/src-tauri/src/proxmox/certificates.rs +++ b/src-tauri/src/proxmox/certificates.rs @@ -45,7 +45,8 @@ pub async fn upload_certificate( .await .map_err(|e| format!("Failed to upload certificate: {}", e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -110,8 +111,6 @@ pub async fn upload_certificate( signature_algorithm, san, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -127,7 +126,8 @@ pub async fn get_certificate( .await .map_err(|e| format!("Failed to get certificate {}: {}", cert_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -192,8 +192,6 @@ pub async fn get_certificate( signature_algorithm, san, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -208,7 +206,7 @@ pub async fn list_certificates( .await .map_err(|e| format!("Failed to list certificates: {}", e))?; - if let Some(certs) = response.get("data").and_then(|d| d.as_array()) { + if let Some(certs) = response.as_array() { let cert_list: Vec = certs .iter() .filter_map(|cert| { @@ -307,7 +305,7 @@ pub async fn list_node_certificates( .await .map_err(|e| format!("Failed to list node certificates for {}: {}", node, e))?; - if let Some(certs) = response.get("data").and_then(|d| d.as_array()) { + if let Some(certs) = response.as_array() { let cert_list: Vec = certs .iter() .filter_map(|cert| { @@ -401,7 +399,8 @@ pub async fn upload_node_certificate( .await .map_err(|e| format!("Failed to upload node certificate for {}: {}", node, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -466,7 +465,5 @@ pub async fn upload_node_certificate( signature_algorithm, san, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } diff --git a/src-tauri/src/proxmox/firewall.rs b/src-tauri/src/proxmox/firewall.rs index 6be52379..3c1f6f89 100644 --- a/src-tauri/src/proxmox/firewall.rs +++ b/src-tauri/src/proxmox/firewall.rs @@ -35,7 +35,7 @@ pub async fn list_firewall_rules( .await .map_err(|e| format!("Failed to list firewall rules: {}", e))?; - if let Some(rules) = response.get("data").and_then(|d| d.as_array()) { + if let Some(rules) = response.as_array() { let rule_list: Vec = rules .iter() .filter_map(|rule| { @@ -68,7 +68,7 @@ pub async fn list_firewall_rules( Ok(rule_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -191,14 +191,12 @@ pub async fn get_firewall_status( .map_err(|e| format!("Failed to get firewall options: {}", e))?; let enabled = options_response - .get("data") - .and_then(|d| d.get("enabled")) + .get("enabled") .and_then(|e| e.as_bool()) .unwrap_or(false); let rules: Vec = rules_response - .get("data") - .and_then(|d| d.as_array()) + .as_array() .unwrap_or(&Vec::new()) .iter() .filter_map(|rule| { @@ -264,10 +262,10 @@ pub async fn list_firewall_zones( .await .map_err(|e| format!("Failed to list firewall zones: {}", e))?; - if let Some(zones) = response.get("data").and_then(|d| d.as_array()) { + if let Some(zones) = response.as_array() { Ok(zones.to_vec()) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } diff --git a/src-tauri/src/proxmox/ha.rs b/src-tauri/src/proxmox/ha.rs index cbddaa28..c9b558e9 100644 --- a/src-tauri/src/proxmox/ha.rs +++ b/src-tauri/src/proxmox/ha.rs @@ -34,7 +34,7 @@ pub async fn list_ha_groups( .await .map_err(|e| format!("Failed to list HA groups: {}", e))?; - if let Some(groups) = response.get("data").and_then(|d| d.as_array()) { + if let Some(groups) = response.as_array() { let group_list: Vec = groups .iter() .filter_map(|group| { @@ -68,7 +68,7 @@ pub async fn list_ha_groups( Ok(group_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -144,7 +144,7 @@ pub async fn list_ha_resources( .await .map_err(|e| format!("Failed to list HA resources: {}", e))?; - if let Some(resources) = response.get("data").and_then(|d| d.as_array()) { + if let Some(resources) = response.as_array() { let resource_list: Vec = resources .iter() .filter_map(|resource| { @@ -179,7 +179,7 @@ pub async fn list_ha_resources( Ok(resource_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } diff --git a/src-tauri/src/proxmox/metrics.rs b/src-tauri/src/proxmox/metrics.rs index 31ffc7b4..a4dae5eb 100644 --- a/src-tauri/src/proxmox/metrics.rs +++ b/src-tauri/src/proxmox/metrics.rs @@ -36,7 +36,8 @@ pub async fn get_node_metrics( .await .map_err(|e| format!("Failed to get node metrics for {}: {}", node, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let cpu = data.get("cpu").and_then(|c| c.as_f64()).unwrap_or(0.0); let memory = data.get("memory").and_then(|m| m.as_f64()).unwrap_or(0.0); let disk = data.get("disk").and_then(|d| d.as_f64()).unwrap_or(0.0); @@ -52,8 +53,6 @@ pub async fn get_node_metrics( load, uptime, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -68,7 +67,7 @@ pub async fn list_nodes( .await .map_err(|e| format!("Failed to list nodes: {}", e))?; - if let Some(resources) = response.get("data").and_then(|d| d.as_array()) { + if let Some(resources) = response.as_array() { let node_list: Vec = resources .iter() .filter_map(|resource| { @@ -107,7 +106,7 @@ pub async fn list_nodes( Ok(node_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } diff --git a/src-tauri/src/proxmox/migration.rs b/src-tauri/src/proxmox/migration.rs index 2267386e..cd8a5d91 100644 --- a/src-tauri/src/proxmox/migration.rs +++ b/src-tauri/src/proxmox/migration.rs @@ -53,7 +53,8 @@ pub async fn migrate_vm( .await .map_err(|e| format!("Failed to migrate VM {}: {}", vm_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let task_id = data .get("taskid") .and_then(|t| t.as_str()) @@ -80,8 +81,6 @@ pub async fn migrate_vm( end_time: None, error: None, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -97,7 +96,7 @@ pub async fn list_migration_status( .await .map_err(|e| format!("Failed to list migration tasks for node {}: {}", node, e))?; - if let Some(tasks) = response.get("data").and_then(|d| d.as_array()) { + if let Some(tasks) = response.as_array() { let task_list: Vec = tasks .iter() .filter_map(|task| { @@ -145,7 +144,7 @@ pub async fn list_migration_status( Ok(task_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } @@ -162,7 +161,8 @@ pub async fn get_migration_task_status( .await .map_err(|e| format!("Failed to get migration task {}: {}", task_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let status = data .get("status") .and_then(|s| s.as_str()) @@ -187,8 +187,6 @@ pub async fn get_migration_task_status( bytes_remaining, downtime, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } diff --git a/src-tauri/src/proxmox/sdn.rs b/src-tauri/src/proxmox/sdn.rs index 2e2a927e..8ec50c13 100644 --- a/src-tauri/src/proxmox/sdn.rs +++ b/src-tauri/src/proxmox/sdn.rs @@ -34,7 +34,7 @@ pub async fn list_evpn_zones( .await .map_err(|e| format!("Failed to list EVPN zones: {}", e))?; - if let Some(zones) = response.get("data").and_then(|d| d.as_array()) { + if let Some(zones) = response.as_array() { let zone_list: Vec = zones .iter() .filter_map(|zone| { @@ -68,7 +68,7 @@ pub async fn list_evpn_zones( Ok(zone_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -140,7 +140,7 @@ pub async fn list_vnets( .await .map_err(|e| format!("Failed to list virtual networks: {}", e))?; - if let Some(vnets) = response.get("data").and_then(|d| d.as_array()) { + if let Some(vnets) = response.as_array() { let vnet_list: Vec = vnets .iter() .filter_map(|vnet| { @@ -166,7 +166,7 @@ pub async fn list_vnets( Ok(vnet_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } @@ -252,10 +252,10 @@ pub async fn list_dhcp_leases( .await .map_err(|e| format!("Failed to list DHCP leases for vnet {}: {}", vnet, e))?; - if let Some(leases) = response.get("data").and_then(|d| d.as_array()) { + if let Some(leases) = response.as_array() { Ok(leases.to_vec()) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Err("Invalid response format".to_string()) } } diff --git a/src-tauri/src/proxmox/shell.rs b/src-tauri/src/proxmox/shell.rs index 97719711..1406f140 100644 --- a/src-tauri/src/proxmox/shell.rs +++ b/src-tauri/src/proxmox/shell.rs @@ -24,7 +24,8 @@ pub async fn get_shell_ticket( .await .map_err(|e| format!("Failed to get shell ticket for remote {}: {}", remote, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let ticket_value = data .get("ticket") .and_then(|t| t.as_str()) @@ -53,8 +54,6 @@ pub async fn get_shell_ticket( expires, permissions, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -69,7 +68,7 @@ pub async fn validate_shell_ticket( .await .map_err(|e| format!("Failed to validate shell ticket: {}", e))?; - Ok(response.get("data").is_some()) + Ok(!response.is_null()) } /// Get shell WebSocket URL diff --git a/src-tauri/src/proxmox/tasks.rs b/src-tauri/src/proxmox/tasks.rs index d01d8945..abb8bc34 100644 --- a/src-tauri/src/proxmox/tasks.rs +++ b/src-tauri/src/proxmox/tasks.rs @@ -38,7 +38,7 @@ pub async fn list_tasks( .await .map_err(|e| format!("Failed to list tasks for node {}: {}", node, e))?; - if let Some(tasks) = response.get("data").and_then(|d| d.as_array()) { + if let Some(tasks) = response.as_array() { let task_list: Vec = tasks .iter() .filter_map(|task| { @@ -97,7 +97,7 @@ pub async fn list_tasks( Ok(task_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } @@ -114,7 +114,8 @@ pub async fn get_task_status( .await .map_err(|e| format!("Failed to get task {}: {}", task_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -169,8 +170,6 @@ pub async fn get_task_status( exit_status, description, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } @@ -206,7 +205,7 @@ pub async fn get_task_log( .await .map_err(|e| format!("Failed to get task log for {}: {}", task_id, e))?; - if let Some(log_entries) = response.get("data").and_then(|d| d.as_array()) { + if let Some(log_entries) = response.as_array() { let log_list: Vec = log_entries .iter() .map(|entry| { @@ -236,7 +235,7 @@ pub async fn get_task_log( Ok(log_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } @@ -258,7 +257,8 @@ pub async fn forward_task( .await .map_err(|e| format!("Failed to forward task {}: {}", task_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -283,7 +283,5 @@ pub async fn forward_task( exit_status: None, description: format!("Forwarded to {}", target_node), }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } diff --git a/src-tauri/src/proxmox/updates.rs b/src-tauri/src/proxmox/updates.rs index daf209b9..22413aea 100644 --- a/src-tauri/src/proxmox/updates.rs +++ b/src-tauri/src/proxmox/updates.rs @@ -34,9 +34,9 @@ pub async fn check_updates( let checked_at = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); let updates: Vec = response - .get("data") - .and_then(|d| d.as_array()) - .unwrap_or(&Vec::new()) + .as_array() + .map(|arr| arr.as_slice()) + .unwrap_or(&[]) .iter() .filter_map(|update| { let package = update.get("package")?.as_str()?.to_string(); @@ -74,8 +74,7 @@ pub async fn list_updates( .map_err(|e| format!("Failed to list updates: {}", e))?; let updates: Vec = response - .get("data") - .and_then(|d| d.as_array()) + .as_array() .map(|arr| { arr.iter() .filter_map(|update| { @@ -153,11 +152,10 @@ pub async fn get_update_history( .await .map_err(|e| format!("Failed to get update history: {}", e))?; - if let Some(history) = response.get("data").and_then(|d| d.as_array()) { - Ok(history.to_vec()) - } else { - Err("Invalid response format: missing 'data' field".to_string()) - } + response + .as_array() + .map(|arr| arr.to_vec()) + .ok_or_else(|| "Invalid response format: expected array".to_string()) } #[cfg(test)] diff --git a/src-tauri/src/proxmox/updates_ext.rs b/src-tauri/src/proxmox/updates_ext.rs index 4c12a79a..4eff15b0 100644 --- a/src-tauri/src/proxmox/updates_ext.rs +++ b/src-tauri/src/proxmox/updates_ext.rs @@ -33,8 +33,7 @@ pub async fn list_updates_all_remotes( .map_err(|e| format!("Failed to list updates from all remotes: {}", e))?; let updates: Vec = response - .get("data") - .and_then(|d| d.as_array()) + .as_array() .map(|arr| { arr.iter() .filter_map(|update| { @@ -122,14 +121,10 @@ pub async fn list_pve_remotes( .await .map_err(|e| format!("Failed to list PVE remotes: {}", e))?; - if let Some(data) = response.get("data") { - if let Some(arr) = data.as_array() { - Ok(arr.to_vec()) - } else { - Ok(vec![data.clone()]) - } + if let Some(arr) = response.as_array() { + Ok(arr.to_vec()) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![response]) } } @@ -146,8 +141,7 @@ pub async fn check_remote_updates( .map_err(|e| format!("Failed to check updates for remote {}: {}", remote, e))?; let updates: Vec = response - .get("data") - .and_then(|d| d.as_array()) + .as_array() .map(|arr| { arr.iter() .filter_map(|update| { diff --git a/src-tauri/src/proxmox/views.rs b/src-tauri/src/proxmox/views.rs index 9ff19044..5b9579df 100644 --- a/src-tauri/src/proxmox/views.rs +++ b/src-tauri/src/proxmox/views.rs @@ -46,7 +46,7 @@ pub async fn list_views( .await .map_err(|e| format!("Failed to list dashboard views: {}", e))?; - if let Some(views) = response.get("data").and_then(|d| d.as_array()) { + if let Some(views) = response.as_array() { let view_list: Vec = views .iter() .filter_map(|view| { @@ -143,7 +143,7 @@ pub async fn list_views( Ok(view_list) } else { - Err("Invalid response format: missing 'data' field".to_string()) + Ok(vec![]) } } @@ -245,7 +245,8 @@ pub async fn get_view( .await .map_err(|e| format!("Failed to get dashboard view {}: {}", view_id, e))?; - if let Some(data) = response.get("data") { + { + let data = &response; let id = data .get("id") .and_then(|i| i.as_str()) @@ -342,7 +343,5 @@ pub async fn get_view( created_at, updated_at, }) - } else { - Err("Invalid response format: missing 'data' field".to_string()) } } diff --git a/src-tauri/src/proxmox/vm.rs b/src-tauri/src/proxmox/vm.rs index 8391d226..872d26b9 100644 --- a/src-tauri/src/proxmox/vm.rs +++ b/src-tauri/src/proxmox/vm.rs @@ -131,57 +131,54 @@ pub async fn list_vms( client: &crate::proxmox::client::ProxmoxClient, ticket: &str, ) -> Result, String> { - let path = "cluster/resources"; - let params = serde_json::json!({ - "type": "qemu" - }); - + // cluster/resources is GET-only; handle_response strips the {"data":[...]} envelope. let response: serde_json::Value = client - .post(path, ¶ms, Some(ticket)) + .get("cluster/resources?type=vm", Some(ticket)) .await .map_err(|e| format!("Failed to list VMs: {}", e))?; - // Parse the response to extract VM info - // The API returns a list of resources in the "data" field - if let Some(resources) = response.get("data").and_then(|d| d.as_array()) { - let vms: Vec = resources - .iter() - .filter_map(|r| { - let vmid = r.get("vmid")?.as_u64()?; - let node = r.get("node")?.as_str()?.to_string(); - let name = r.get("name")?.as_str().map(|s| s.to_string()); - let status = r.get("status")?.as_str()?.to_string(); - let cpu = r.get("cpu")?.as_f64()?; + let resources = response + .as_array() + .ok_or_else(|| "Invalid response format".to_string())?; - Some(VmInfo { - id: vmid as u32, - name, - status, - cpu, - memory: r.get("mem").and_then(|m| m.as_u64()).unwrap_or(0), - disk: r.get("disk").and_then(|d| d.as_u64()).unwrap_or(0), - uptime: r.get("uptime").and_then(|u| u.as_u64()).unwrap_or(0), - node, - template: r.get("template").and_then(|t| t.as_bool()), - agent: r - .get("agent") - .and_then(|a| a.as_str()) - .map(|s| s.to_string()), - mem: r.get("mem").and_then(|m| m.as_u64()), - max_mem: r.get("maxmem").and_then(|m| m.as_u64()), - max_disk: r.get("maxdisk").and_then(|d| d.as_u64()), - netin: r.get("netin").and_then(|n| n.as_u64()), - netout: r.get("netout").and_then(|n| n.as_u64()), - diskread: r.get("diskread").and_then(|d| d.as_u64()), - diskwrite: r.get("diskwrite").and_then(|d| d.as_u64()), - }) + let vms: Vec = resources + .iter() + .filter_map(|r| { + let vmid = r.get("vmid")?.as_u64()?; + let node = r.get("node")?.as_str()?.to_string(); + // Only include qemu VMs (not LXC containers which also appear in cluster/resources?type=vm) + let resource_type = r.get("type").and_then(|t| t.as_str()).unwrap_or(""); + if resource_type != "qemu" { + return None; + } + let name = r.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()); + let status = r.get("status").and_then(|s| s.as_str()).unwrap_or("unknown").to_string(); + // cpu may be absent for stopped VMs + let cpu = r.get("cpu").and_then(|c| c.as_f64()).unwrap_or(0.0); + + Some(VmInfo { + id: vmid as u32, + name, + status, + cpu, + memory: r.get("mem").and_then(|m| m.as_u64()).unwrap_or(0), + disk: r.get("disk").and_then(|d| d.as_u64()).unwrap_or(0), + uptime: r.get("uptime").and_then(|u| u.as_u64()).unwrap_or(0), + node, + template: r.get("template").and_then(|t| t.as_bool()), + agent: r.get("agent").and_then(|a| a.as_str()).map(|s| s.to_string()), + mem: r.get("mem").and_then(|m| m.as_u64()), + max_mem: r.get("maxmem").and_then(|m| m.as_u64()), + max_disk: r.get("maxdisk").and_then(|d| d.as_u64()), + netin: r.get("netin").and_then(|n| n.as_u64()), + netout: r.get("netout").and_then(|n| n.as_u64()), + diskread: r.get("diskread").and_then(|d| d.as_u64()), + diskwrite: r.get("diskwrite").and_then(|d| d.as_u64()), }) - .collect(); + }) + .collect(); - Ok(vms) - } else { - Err("Invalid response format: missing 'data' field".to_string()) - } + Ok(vms) } /// Get VM details @@ -197,8 +194,7 @@ pub async fn get_vm( .await .map_err(|e| format!("Failed to get VM {}: {}", vmid, e))?; - // Parse the response to extract VM info - let vm = response.get("data").ok_or("Invalid response format")?; + let vm = &response; Ok(VmInfo { id: vmid, @@ -415,11 +411,10 @@ pub async fn list_snapshots( .await .map_err(|e| format!("Failed to list snapshots for VM {}: {}", vmid, e))?; - if let Some(snapshots) = response.get("data").and_then(|d| d.as_array()) { - Ok(snapshots.to_vec()) - } else { - Err("Invalid response format: missing 'data' field".to_string()) - } + response + .as_array() + .map(|arr| arr.to_vec()) + .ok_or_else(|| "Invalid response format".to_string()) } #[cfg(test)]