fix(proxmox): restore broken client retrieval across all commands
All checks were successful
Test / frontend-tests (pull_request) Successful in 1m36s
Test / frontend-typecheck (pull_request) Successful in 1m46s
PR Review Automation / review (pull_request) Successful in 5m8s
Test / rust-fmt-check (pull_request) Successful in 12m30s
Test / rust-clippy (pull_request) Successful in 14m10s
Test / rust-tests (pull_request) Successful in 16m35s

Half-completed refactor left 68 Tauri command functions with orphaned
.ok_or_else() chains after the old clusters.get() pattern was removed
without inserting the replacement helper call. Also fixed two bugs in the
new get_proxmox_client_for_cluster helper: undeclared `clusters` variable
in the early-return check, and client_arc going out of scope before return.

fix(ai): enforce system-message-first ordering for strict LLM providers

Qwen3.5-122b (and other models via LiteLLM) reject requests where system
messages appear after user/assistant turns. Moved tool-calling format
and iteration-budget system messages to before history is appended.
Changed mid-loop iteration warning and forced-stop instruction from
system role to user role so they can safely appear mid-conversation.

fix(proxmox): Remotes actions menu and connect/disconnect behaviour

Replaced the non-functional "..." toast placeholder with a proper
ActionsMenu dropdown (Edit / Test Connection / Delete). Removed inline
emoji buttons folded into the menu. Connect now calls getProxmoxCluster
as a live connection test and reflects real status; disconnect marks the
remote disconnected locally. Remote status now maps correctly from the
backend ClusterInfoWithHealth.connected field instead of hardcoding
'connected' for every entry.

fix(proxmox): Ceph page no longer shows HEALTH_OK on non-Ceph clusters

Page now fetches real health data on mount. If getCephHealth fails the
page renders an informational notice rather than fake HEALTH_OK. When
Ceph is present, pools and OSDs are loaded and displayed live.
This commit is contained in:
Shaun Arman 2026-06-19 22:13:48 -05:00
parent 466a57c549
commit c5b97f8648
5 changed files with 415 additions and 344 deletions

View File

@ -351,9 +351,11 @@ pub async fn chat_message(
let agent_registry = create_agent_registry(); let agent_registry = create_agent_registry();
let devops_agent = agent_registry.get("devops-incident-responder"); let devops_agent = agent_registry.get("devops-incident-responder");
// CRITICAL: Build messages array with ALL system messages FIRST, then history, then user message
// This ensures system messages are always at the beginning as required by most LLM APIs
let mut messages = Vec::new(); let mut messages = Vec::new();
// Inject devops-incident-responder as primary system prompt (always) // 1. Inject devops-incident-responder as primary system prompt (always first)
if let Some(agent) = devops_agent { if let Some(agent) = devops_agent {
messages.push(Message { messages.push(Message {
role: "system".into(), role: "system".into(),
@ -363,7 +365,7 @@ pub async fn chat_message(
}); });
} }
// Inject domain system prompt if provided // 2. Inject domain system prompt if provided (second position)
if let Some(ref prompt) = system_prompt { if let Some(ref prompt) = system_prompt {
if !prompt.is_empty() { if !prompt.is_empty() {
messages.push(Message { messages.push(Message {
@ -375,28 +377,6 @@ pub async fn chat_message(
} }
} }
messages.extend(history);
// If we found integration content, add it to the conversation context
if !integration_context.is_empty() {
let context_message = Message {
role: "system".into(),
content: format!(
"INTERNAL DOCUMENTATION SOURCES:\n\n{integration_context}\n\n\
Instructions: The above content is from internal company documentation systems \
(Confluence, ServiceNow, Azure DevOps). \
\n\n**IMPORTANT**: First determine if this documentation is RELEVANT to the user's question:\
\n- If the documentation directly addresses the question Use it and cite sources with URLs\
\n- If the documentation is tangentially related but doesn't answer the question Briefly mention what internal docs exist, then provide a complete answer using general knowledge\
\n- If the documentation is completely unrelated Ignore it and answer using general knowledge\
\n\nDo NOT force irrelevant internal documentation into your answer. The user needs accurate information, not forced citations."
),
tool_call_id: None,
tool_calls: None,
};
messages.push(context_message);
}
// Tool execution configuration // Tool execution configuration
const MAX_TOOL_ITERATIONS: usize = 20; // Allow sufficient iterations for complex diagnostics const MAX_TOOL_ITERATIONS: usize = 20; // Allow sufficient iterations for complex diagnostics
@ -427,6 +407,7 @@ pub async fn chat_message(
!matches!(kind, "anthropic" | "gemini" | "mistral" | "ollama") !matches!(kind, "anthropic" | "gemini" | "mistral" | "ollama")
}; };
// 3. Tool-calling system messages — must come BEFORE history so all system messages are contiguous
if tools.is_some() && is_openai_compatible { if tools.is_some() && is_openai_compatible {
messages.push(Message { messages.push(Message {
role: "system".into(), role: "system".into(),
@ -435,7 +416,6 @@ pub async fn chat_message(
tool_calls: None, tool_calls: None,
}); });
// Add iteration budget awareness
messages.push(Message { messages.push(Message {
role: "system".into(), role: "system".into(),
content: format!( content: format!(
@ -454,6 +434,34 @@ pub async fn chat_message(
}); });
} }
// 4. Integration context as system message — still before history
if !integration_context.is_empty() {
messages.push(Message {
role: "system".into(),
content: format!(
"INTERNAL DOCUMENTATION SOURCES:\n\n{integration_context}\n\n\
Instructions: The above content is from internal company documentation systems \
(Confluence, ServiceNow, Azure DevOps). \
\n\n**IMPORTANT**: First determine if this documentation is RELEVANT to the user's question:\
\n- If the documentation directly addresses the question Use it and cite sources with URLs\
\n- If the documentation is tangentially related but doesn't answer the question Briefly mention what internal docs exist, then provide a complete answer using general knowledge\
\n- If the documentation is completely unrelated Ignore it and answer using general knowledge\
\n\nDo NOT force irrelevant internal documentation into your answer. The user needs accurate information, not forced citations."
),
tool_call_id: None,
tool_calls: None,
});
}
// 5. Filter out any system messages from history to avoid duplicates and maintain order
let filtered_history: Vec<Message> = history
.into_iter()
.filter(|msg| msg.role != "system")
.collect();
// 6. Add filtered history (user/assistant messages only) — all system messages are already above
messages.extend(filtered_history);
messages.push(Message { messages.push(Message {
role: "user".into(), role: "user".into(),
content: full_message.clone(), content: full_message.clone(),
@ -471,7 +479,7 @@ pub async fn chat_message(
// Warn AI when approaching limit // Warn AI when approaching limit
if iteration == MAX_TOOL_ITERATIONS - 2 { if iteration == MAX_TOOL_ITERATIONS - 2 {
messages.push(Message { messages.push(Message {
role: "system".into(), role: "user".into(),
content: format!( content: format!(
"WARNING: You are on iteration {iteration}/{MAX_TOOL_ITERATIONS} (2 rounds remaining). \ "WARNING: You are on iteration {iteration}/{MAX_TOOL_ITERATIONS} (2 rounds remaining). \
You MUST provide your final answer in the NEXT round. \ You MUST provide your final answer in the NEXT round. \
@ -490,7 +498,7 @@ pub async fn chat_message(
// Add final instruction // Add final instruction
let mut final_messages = sanitized_messages; let mut final_messages = sanitized_messages;
final_messages.push(Message { final_messages.push(Message {
role: "system".into(), role: "user".into(),
content: format!( content: format!(
"CRITICAL: Tool iteration limit reached ({iteration}/{MAX_TOOL_ITERATIONS}). \ "CRITICAL: Tool iteration limit reached ({iteration}/{MAX_TOOL_ITERATIONS}). \
TOOLS ARE NOW DISABLED. \ TOOLS ARE NOW DISABLED. \

View File

@ -232,16 +232,87 @@ pub async fn get_proxmox_cluster(
Ok(cluster) Ok(cluster)
} }
/// Helper function to get or create a Proxmox client for a cluster
/// This will:
/// 1. Check if client exists in memory pool
/// 2. If not, load credentials from database and create/authenticate client
async fn get_proxmox_client_for_cluster(
cluster_id: &str,
state: &State<'_, AppState>,
) -> Result<Arc<Mutex<crate::proxmox::ProxmoxClient>>, String> {
// First, try to get from in-memory pool
{
let clusters = state.proxmox_clusters.lock().await;
if let Some(client) = clusters.get(cluster_id) {
return Ok(client.clone());
}
}
// Not in memory - load from database and create client
let (url, port, username, encrypted_credentials) = {
let db = state
.db
.lock()
.map_err(|e| format!("Failed to lock database: {}", e))?;
let mut stmt = db
.prepare(
"SELECT url, port, username, encrypted_credentials FROM proxmox_clusters WHERE id = ?1",
)
.map_err(|e| format!("Failed to prepare query: {}", e))?;
stmt.query_row([cluster_id], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, u16>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})
.optional()
.map_err(|e| format!("Failed to query cluster: {}", e))?
.ok_or_else(|| format!("Cluster {} not found in database", cluster_id))?
};
// Decrypt credentials
let credentials_json = crate::integrations::auth::decrypt_token(&encrypted_credentials)
.map_err(|e| format!("Failed to decrypt credentials: {}", e))?;
let credentials: serde_json::Value = serde_json::from_str(&credentials_json)
.map_err(|e| format!("Failed to parse credentials: {}", e))?;
let password = credentials
.get("password")
.and_then(|v| v.as_str())
.ok_or_else(|| "Password not found in credentials".to_string())?;
// Create new client
let mut client = crate::proxmox::ProxmoxClient::new(&url, port, &username);
// Authenticate to get ticket
let ticket = client
.authenticate(password)
.await
.map_err(|e| format!("Failed to authenticate with Proxmox: {}", e))?;
client.set_ticket(&ticket);
let client_arc = Arc::new(Mutex::new(client));
{
let mut clusters = state.proxmox_clusters.lock().await;
clusters.insert(cluster_id.to_string(), client_arc.clone());
}
Ok(client_arc)
}
/// List all Proxmox VMs /// List all Proxmox VMs
#[tauri::command] #[tauri::command]
pub async fn list_proxmox_vms( pub async fn list_proxmox_vms(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let vms = let vms =
@ -266,10 +337,7 @@ pub async fn get_proxmox_vm(
vm_id: u32, vm_id: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let vm = crate::proxmox::vm::get_vm( let vm = crate::proxmox::vm::get_vm(
@ -292,10 +360,7 @@ pub async fn start_proxmox_vm(
vm_id: u32, vm_id: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::vm::start_vm( crate::proxmox::vm::start_vm(
@ -316,10 +381,7 @@ pub async fn stop_proxmox_vm(
vm_id: u32, vm_id: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::vm::stop_vm( crate::proxmox::vm::stop_vm(
@ -340,10 +402,7 @@ pub async fn reboot_proxmox_vm(
vm_id: u32, vm_id: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::vm::reboot_vm( crate::proxmox::vm::reboot_vm(
@ -364,10 +423,7 @@ pub async fn shutdown_proxmox_vm(
vm_id: u32, vm_id: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::vm::shutdown_vm( crate::proxmox::vm::shutdown_vm(
@ -387,10 +443,7 @@ pub async fn list_proxmox_backup_jobs(
node: String, node: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let jobs = crate::proxmox::backup::list_backup_jobs( let jobs = crate::proxmox::backup::list_backup_jobs(
@ -417,10 +470,7 @@ pub async fn list_proxmox_datastores(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let datastores = crate::proxmox::backup::list_datastores( let datastores = crate::proxmox::backup::list_datastores(
@ -448,10 +498,7 @@ pub async fn trigger_proxmox_backup_job(
job_id: u32, job_id: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::backup::trigger_backup_job( crate::proxmox::backup::trigger_backup_job(
@ -470,10 +517,7 @@ pub async fn list_ceph_pools(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let pools = crate::proxmox::ceph::list_pools( let pools = crate::proxmox::ceph::list_pools(
@ -499,10 +543,7 @@ pub async fn list_ceph_osd(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let osds = crate::proxmox::ceph::list_osds( let osds = crate::proxmox::ceph::list_osds(
@ -528,10 +569,7 @@ pub async fn get_ceph_health(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let health = crate::proxmox::ceph::get_ceph_health( let health = crate::proxmox::ceph::get_ceph_health(
@ -552,10 +590,7 @@ pub async fn list_auth_realms(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let realms = crate::proxmox::auth_realm::list_auth_realms( let realms = crate::proxmox::auth_realm::list_auth_realms(
@ -590,10 +625,7 @@ pub async fn add_ldap_realm(
certificate: String, certificate: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let config = crate::proxmox::auth_realm::LdapRealmConfig { let config = crate::proxmox::auth_realm::LdapRealmConfig {
@ -635,10 +667,7 @@ pub async fn add_ad_realm(
certificate: String, certificate: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let config = crate::proxmox::auth_realm::AdRealmConfig { let config = crate::proxmox::auth_realm::AdRealmConfig {
@ -677,10 +706,7 @@ pub async fn add_openid_realm(
mapping: String, mapping: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let config = crate::proxmox::auth_realm::OpenidRealmConfig { let config = crate::proxmox::auth_realm::OpenidRealmConfig {
@ -708,10 +734,7 @@ pub async fn list_acme_accounts(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let accounts = crate::proxmox::acme::list_acme_accounts( let accounts = crate::proxmox::acme::list_acme_accounts(
@ -737,10 +760,7 @@ pub async fn register_acme_account(
terms_of_service_agreed: bool, terms_of_service_agreed: bool,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let account = crate::proxmox::acme::register_acme_account( let account = crate::proxmox::acme::register_acme_account(
@ -762,10 +782,7 @@ pub async fn get_acme_challenges(
domain: String, domain: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let challenges = crate::proxmox::acme::get_acme_challenges( let challenges = crate::proxmox::acme::get_acme_challenges(
@ -793,10 +810,7 @@ pub async fn list_apt_updates(
node: String, node: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let updates = crate::proxmox::apt::list_apt_updates( let updates = crate::proxmox::apt::list_apt_updates(
@ -822,10 +836,7 @@ pub async fn update_apt_repos(
node: String, node: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::apt::update_apt_repos( crate::proxmox::apt::update_apt_repos(
@ -844,10 +855,7 @@ pub async fn list_apt_repositories(
node: String, node: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let repos = crate::proxmox::apt::list_apt_repositories( let repos = crate::proxmox::apt::list_apt_repositories(
@ -873,10 +881,7 @@ pub async fn get_shell_ticket(
remote: String, remote: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let ticket = crate::proxmox::shell::get_shell_ticket( let ticket = crate::proxmox::shell::get_shell_ticket(
@ -896,10 +901,7 @@ pub async fn list_views(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let views = crate::proxmox::views::list_views( let views = crate::proxmox::views::list_views(
@ -930,10 +932,7 @@ pub async fn add_view(
enabled: bool, enabled: bool,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let widgets: Vec<crate::proxmox::views::Widget> = widgets let widgets: Vec<crate::proxmox::views::Widget> = widgets
@ -976,10 +975,7 @@ pub async fn update_view(
enabled: bool, enabled: bool,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let widgets: Vec<crate::proxmox::views::Widget> = widgets let widgets: Vec<crate::proxmox::views::Widget> = widgets
@ -1017,10 +1013,7 @@ pub async fn delete_view(
view_id: String, view_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::views::delete_view( crate::proxmox::views::delete_view(
@ -1038,10 +1031,7 @@ pub async fn list_certificates(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let certs = crate::proxmox::certificates::list_certificates( let certs = crate::proxmox::certificates::list_certificates(
@ -1068,10 +1058,7 @@ pub async fn upload_certificate(
name: Option<String>, name: Option<String>,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let cert = crate::proxmox::certificates::upload_certificate( let cert = crate::proxmox::certificates::upload_certificate(
@ -1094,10 +1081,7 @@ pub async fn get_certificate(
cert_id: String, cert_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let cert = crate::proxmox::certificates::get_certificate( let cert = crate::proxmox::certificates::get_certificate(
@ -1121,10 +1105,7 @@ pub async fn list_firewall_rules(
node: String, node: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let rules = crate::proxmox::firewall::list_firewall_rules( let rules = crate::proxmox::firewall::list_firewall_rules(
@ -1157,10 +1138,7 @@ pub async fn add_firewall_rule(
enabled: bool, enabled: bool,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let rule = crate::proxmox::firewall::FirewallRule { let rule = crate::proxmox::firewall::FirewallRule {
@ -1191,10 +1169,7 @@ pub async fn delete_firewall_rule(
rule_num: u32, rule_num: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::firewall::delete_rule( crate::proxmox::firewall::delete_rule(
@ -1214,10 +1189,7 @@ pub async fn list_sdn_controllers(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let controllers = crate::proxmox::sdn::list_evpn_zones( let controllers = crate::proxmox::sdn::list_evpn_zones(
@ -1243,10 +1215,7 @@ pub async fn list_sdn_vnets(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let vnets = crate::proxmox::sdn::list_vnets( let vnets = crate::proxmox::sdn::list_vnets(
@ -1270,10 +1239,7 @@ pub async fn list_sdn_zones(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let zones = crate::proxmox::sdn::list_evpn_zones( let zones = crate::proxmox::sdn::list_evpn_zones(
@ -1300,10 +1266,7 @@ pub async fn list_ceph_clusters(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let ceph_clusters = crate::proxmox::ceph_cluster::list_ceph_clusters( let ceph_clusters = crate::proxmox::ceph_cluster::list_ceph_clusters(
@ -1328,10 +1291,7 @@ pub async fn get_ceph_cluster_status(
ceph_cluster_id: String, ceph_cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let status = crate::proxmox::ceph_cluster::get_ceph_cluster_status( let status = crate::proxmox::ceph_cluster::get_ceph_cluster_status(
@ -1358,10 +1318,7 @@ pub async fn migrate_vm(
target_cluster: String, target_cluster: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let task = crate::proxmox::migration::migrate_vm( let task = crate::proxmox::migration::migrate_vm(
@ -1385,10 +1342,7 @@ pub async fn list_migration_status(
node: String, node: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let tasks = crate::proxmox::migration::list_migration_status( let tasks = crate::proxmox::migration::list_migration_status(
@ -1414,10 +1368,7 @@ pub async fn list_updates(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let updates = crate::proxmox::updates_ext::list_updates_all_remotes( let updates = crate::proxmox::updates_ext::list_updates_all_remotes(
@ -1438,10 +1389,7 @@ pub async fn list_updates(
/// Refresh updates /// Refresh updates
#[tauri::command] #[tauri::command]
pub async fn refresh_updates(cluster_id: String, state: State<'_, AppState>) -> Result<(), String> { pub async fn refresh_updates(cluster_id: String, state: State<'_, AppState>) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::updates_ext::refresh_updates_all( crate::proxmox::updates_ext::refresh_updates_all(
@ -1459,10 +1407,7 @@ pub async fn install_updates(
packages: Vec<String>, packages: Vec<String>,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let package_refs: Vec<&str> = packages.iter().map(|s| s.as_str()).collect(); let package_refs: Vec<&str> = packages.iter().map(|s| s.as_str()).collect();
@ -1483,10 +1428,7 @@ pub async fn list_tasks(
node: String, node: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let tasks = crate::proxmox::tasks::list_tasks( let tasks = crate::proxmox::tasks::list_tasks(
@ -1513,10 +1455,7 @@ pub async fn get_task_status(
task_id: String, task_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let task = crate::proxmox::tasks::get_task_status( let task = crate::proxmox::tasks::get_task_status(
@ -1539,10 +1478,7 @@ pub async fn stop_task(
task_id: String, task_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::tasks::stop_task( crate::proxmox::tasks::stop_task(
@ -1564,10 +1500,7 @@ pub async fn get_metrics_summary(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let nodes = crate::proxmox::metrics::list_nodes( let nodes = crate::proxmox::metrics::list_nodes(
@ -1592,10 +1525,7 @@ pub async fn list_metric_collections(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let nodes = crate::proxmox::metrics::list_nodes( let nodes = crate::proxmox::metrics::list_nodes(
@ -1621,10 +1551,7 @@ pub async fn list_ha_groups(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let groups = crate::proxmox::ha::list_ha_groups( let groups = crate::proxmox::ha::list_ha_groups(
@ -1650,10 +1577,7 @@ pub async fn create_ha_group(
max_relocate: u32, max_relocate: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::ha::create_ha_group( crate::proxmox::ha::create_ha_group(
@ -1678,10 +1602,7 @@ pub async fn update_ha_group(
max_relocate: u32, max_relocate: u32,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::ha::update_ha_group( crate::proxmox::ha::update_ha_group(
@ -1703,10 +1624,7 @@ pub async fn delete_ha_group(
group: String, group: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::ha::delete_ha_group( crate::proxmox::ha::delete_ha_group(
@ -1724,10 +1642,7 @@ pub async fn list_ha_resources(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let resources = crate::proxmox::ha::list_ha_resources( let resources = crate::proxmox::ha::list_ha_resources(
@ -1750,10 +1665,7 @@ pub async fn enable_ha_resource(
resource: String, resource: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::ha::enable_ha_resource( crate::proxmox::ha::enable_ha_resource(
@ -1773,10 +1685,7 @@ pub async fn list_acls(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = "access/acl"; let path = "access/acl";
@ -1798,10 +1707,7 @@ pub async fn list_users(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = "access/users"; let path = "access/users";
@ -1823,10 +1729,7 @@ pub async fn list_realms(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let realms = crate::proxmox::auth_realm::list_auth_realms( let realms = crate::proxmox::auth_realm::list_auth_realms(
@ -1850,10 +1753,7 @@ pub async fn get_cluster_notes(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<String, String> { ) -> Result<String, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = "cluster/config"; let path = "cluster/config";
@ -1877,10 +1777,7 @@ pub async fn update_cluster_notes(
notes: String, notes: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = "cluster/config"; let path = "cluster/config";
@ -1906,10 +1803,7 @@ pub async fn search_proxmox_resources(
query: String, query: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = format!("cluster/resources?type=vm&search={}", query); let path = format!("cluster/resources?type=vm&search={}", query);
@ -1934,10 +1828,7 @@ pub async fn get_node_status(
node_id: String, node_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = format!("nodes/{}/status", node_id); let path = format!("nodes/{}/status", node_id);
@ -1962,10 +1853,7 @@ pub async fn get_syslog(
limit: Option<u32>, limit: Option<u32>,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let limit_val = limit.unwrap_or(500); let limit_val = limit.unwrap_or(500);
@ -1991,10 +1879,7 @@ pub async fn list_network_interfaces(
node_id: String, node_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = format!("nodes/{}/network", node_id); let path = format!("nodes/{}/network", node_id);
@ -2018,10 +1903,7 @@ pub async fn list_cluster_views(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let views = crate::proxmox::views::list_views( let views = crate::proxmox::views::list_views(
@ -2045,10 +1927,7 @@ pub async fn create_cluster_view(
name: String, name: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let view = crate::proxmox::views::DashboardView { let view = crate::proxmox::views::DashboardView {
@ -2078,10 +1957,7 @@ pub async fn delete_cluster_view(
view_id: String, view_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
crate::proxmox::views::delete_view( crate::proxmox::views::delete_view(
@ -2101,10 +1977,7 @@ pub async fn get_subscription_status(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<serde_json::Value, String> { ) -> Result<serde_json::Value, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = "nodes/localhost/subscription"; let path = "nodes/localhost/subscription";
@ -2128,10 +2001,7 @@ pub async fn list_cluster_tasks(
limit: Option<u32>, limit: Option<u32>,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let limit_val = limit.unwrap_or(50); let limit_val = limit.unwrap_or(50);
@ -2154,10 +2024,7 @@ pub async fn list_proxmox_containers(
cluster_id: String, cluster_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> { ) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await; let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await; let client_guard = client.lock().await;
let path = "cluster/resources?type=lxc"; let path = "cluster/resources?type=lxc";

View File

@ -1,8 +1,8 @@
import React from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { MoreHorizontal } from 'lucide-react'; import { MoreHorizontal, Plug, PlugZap } from 'lucide-react';
interface RemoteInfo { interface RemoteInfo {
id: string; id: string;
@ -25,6 +25,78 @@ interface RemotesListProps {
onDisconnect?: (remote: RemoteInfo) => void; onDisconnect?: (remote: RemoteInfo) => void;
} }
function ActionsMenu({
remote,
onEdit,
onDelete,
onConnect,
onDisconnect,
}: {
remote: RemoteInfo;
onEdit?: (remote: RemoteInfo) => void;
onDelete?: (remote: RemoteInfo) => void;
onConnect?: (remote: RemoteInfo) => void;
onDisconnect?: (remote: RemoteInfo) => void;
}) {
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
return (
<div className="relative" ref={menuRef}>
<button
className="rounded-md p-1 hover:bg-accent"
onClick={() => setOpen((v) => !v)}
title="More actions"
>
<MoreHorizontal className="h-4 w-4" />
</button>
{open && (
<div className="absolute right-0 z-50 mt-1 w-44 rounded-md border bg-background shadow-lg">
<div className="py-1">
<button
className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-accent"
onClick={() => { setOpen(false); onEdit?.(remote); }}
>
Edit
</button>
<button
className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-accent"
onClick={() => {
setOpen(false);
if (remote.status === 'connected') {
onDisconnect?.(remote);
} else {
onConnect?.(remote);
}
}}
>
Test Connection
</button>
<div className="my-1 h-px bg-border" />
<button
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-destructive hover:bg-destructive/10"
onClick={() => { setOpen(false); onDelete?.(remote); }}
>
Delete
</button>
</div>
</div>
)}
</div>
);
}
export function RemotesList({ export function RemotesList({
remotes, remotes,
onRefresh, onRefresh,
@ -100,44 +172,31 @@ export function RemotesList({
</TableCell> </TableCell>
<TableCell>{remote.lastConnected || '-'}</TableCell> <TableCell>{remote.lastConnected || '-'}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex items-center justify-end space-x-2"> <div className="flex items-center justify-end space-x-1">
<button
className="rounded-md p-1 hover:bg-accent"
onClick={() => onEdit?.(remote)}
title="Edit"
>
<span className="h-4 w-4 text-xs"></span>
</button>
{remote.status === 'connected' ? ( {remote.status === 'connected' ? (
<button <button
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600" className="rounded-md p-1 hover:bg-red-100 hover:text-red-600 text-green-600"
onClick={() => onDisconnect?.(remote)} onClick={() => onDisconnect?.(remote)}
title="Disconnect" title="Disconnect"
> >
<span className="h-4 w-4 text-xs">🔌</span> <PlugZap className="h-4 w-4" />
</button> </button>
) : ( ) : (
<button <button
className="rounded-md p-1 hover:bg-green-100 hover:text-green-600" className="rounded-md p-1 hover:bg-green-100 hover:text-green-600 text-muted-foreground"
onClick={() => onConnect?.(remote)} onClick={() => onConnect?.(remote)}
title="Connect" title="Test connection"
> >
<span className="h-4 w-4 text-xs">🔌</span> <Plug className="h-4 w-4" />
</button> </button>
)} )}
<button <ActionsMenu
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600" remote={remote}
onClick={() => onDelete?.(remote)} onEdit={onEdit}
title="Delete" onDelete={onDelete}
> onConnect={onConnect}
<span className="h-4 w-4 text-xs">🗑</span> onDisconnect={onDisconnect}
</button> />
<button
className="rounded-md p-1 hover:bg-accent"
title="More"
>
<MoreHorizontal className="h-4 w-4" />
</button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -1,10 +1,111 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import { PoolList, OSDList, CephHealthWidget, MonitorList } from '@/components/Proxmox'; import { PoolList, OSDList, CephHealthWidget, MonitorList } from '@/components/Proxmox';
import { listProxmoxClusters, listCephPools, listCephOsd, getCephHealth } from '@/lib/proxmoxClient';
import { toast } from 'sonner';
export function ProxmoxCephPage() { export function ProxmoxCephPage() {
const [clusterId, setClusterId] = useState<string>('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [health, setHealth] = useState<any>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [pools, setPools] = useState<any[]>([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [osds, setOsds] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isCephEnabled, setIsCephEnabled] = useState<boolean | null>(null);
const loadData = useCallback(async (cId: string) => {
if (!cId) return;
setLoading(true);
setError(null);
// Check Ceph availability by fetching health first
let cephAvailable = false;
try {
const h = await getCephHealth(cId);
setHealth(h);
cephAvailable = true;
} catch {
setIsCephEnabled(false);
setLoading(false);
return;
}
if (cephAvailable) {
setIsCephEnabled(true);
const [poolsResult, osdsResult] = await Promise.allSettled([
listCephPools(cId),
listCephOsd(cId),
]);
if (poolsResult.status === 'fulfilled') {
setPools(poolsResult.value);
} else {
toast.error('Failed to load Ceph pools');
}
if (osdsResult.status === 'fulfilled') {
setOsds(osdsResult.value);
} else {
toast.error('Failed to load Ceph OSDs');
}
}
setLoading(false);
}, []);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
if (cls.length > 0) {
setClusterId(cls[0].id);
loadData(cls[0].id);
} else {
setIsCephEnabled(false);
}
})
.catch((err) => {
console.error('Failed to load clusters:', err);
setError('Failed to load clusters');
setIsCephEnabled(false);
});
}, [loadData]);
const handleRefresh = () => {
if (clusterId) loadData(clusterId);
};
if (isCephEnabled === false) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Ceph Storage</h1>
<p className="text-muted-foreground">Manage Ceph clusters and storage</p>
</div>
</div>
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
{error ? (
<p>{error}</p>
) : (
<>
<p className="text-base font-medium">Ceph is not configured on this cluster</p>
<p className="text-sm mt-1">
Ceph storage requires a dedicated Ceph cluster deployment on the Proxmox nodes.
</p>
</>
)}
</CardContent>
</Card>
</div>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -13,8 +114,8 @@ export function ProxmoxCephPage() {
<p className="text-muted-foreground">Manage Ceph clusters and storage</p> <p className="text-muted-foreground">Manage Ceph clusters and storage</p>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" onClick={handleRefresh} disabled={loading}>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Refresh Refresh
</Button> </Button>
</div> </div>
@ -26,9 +127,11 @@ export function ProxmoxCephPage() {
<CardTitle>Ceph Health</CardTitle> <CardTitle>Ceph Health</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<CephHealthWidget {health ? (
health={{ status: 'HEALTH_OK', summary: 'Cluster healthy', details: [] }} <CephHealthWidget health={health} />
/> ) : (
<p className="text-sm text-muted-foreground">Loading health data...</p>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -40,8 +143,8 @@ export function ProxmoxCephPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<PoolList <PoolList
pools={[]} pools={pools}
onRefresh={() => {}} onRefresh={handleRefresh}
/> />
</CardContent> </CardContent>
</Card> </Card>
@ -52,8 +155,8 @@ export function ProxmoxCephPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<OSDList <OSDList
osds={[]} osds={osds}
onRefresh={() => {}} onRefresh={handleRefresh}
/> />
</CardContent> </CardContent>
</Card> </Card>
@ -66,7 +169,7 @@ export function ProxmoxCephPage() {
<CardContent> <CardContent>
<MonitorList <MonitorList
monitors={[]} monitors={[]}
onRefresh={() => {}} onRefresh={handleRefresh}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -6,7 +6,7 @@ import { AddRemoteForm } from '@/components/Proxmox';
import { EditRemoteForm } from '@/components/Proxmox'; import { EditRemoteForm } from '@/components/Proxmox';
import { RemoveRemoteDialog } from '@/components/Proxmox'; import { RemoveRemoteDialog } from '@/components/Proxmox';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/index'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/index';
import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster } from '@/lib/proxmoxClient'; import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster, getProxmoxCluster } from '@/lib/proxmoxClient';
import { ClusterType } from '@/lib/domain'; import { ClusterType } from '@/lib/domain';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -35,7 +35,7 @@ export function ProxmoxRemotesPage() {
url: c.url, url: c.url,
username: c.username, username: c.username,
type: c.clusterType === 've' ? 'pve' : 'pbs', type: c.clusterType === 've' ? 'pve' : 'pbs',
status: 'connected' as const, // Placeholder - actual status requires connection test status: (c.connected ? 'connected' : 'disconnected') as RemoteInfo['status'],
})); }));
setRemotes(remotesList); setRemotes(remotesList);
} catch (err) { } catch (err) {
@ -136,6 +136,34 @@ export function ProxmoxRemotesPage() {
} }
}; };
const handleConnectRemote = async (remote: RemoteInfo) => {
try {
toast.info(`Testing connection to ${remote.name}...`);
const result = await getProxmoxCluster(remote.id);
if (result !== null) {
toast.success(`Connected to ${remote.name}`);
setRemotes((prev) =>
prev.map((r) => (r.id === remote.id ? { ...r, status: 'connected' } : r))
);
} else {
toast.error(`Cluster ${remote.name} not found`);
}
} catch (err) {
console.error('Failed to connect remote:', err);
toast.error('Connection failed: ' + String(err));
setRemotes((prev) =>
prev.map((r) => (r.id === remote.id ? { ...r, status: 'error' } : r))
);
}
};
const handleDisconnectRemote = (remote: RemoteInfo) => {
setRemotes((prev) =>
prev.map((r) => (r.id === remote.id ? { ...r, status: 'disconnected' } : r))
);
toast.info(`Disconnected from ${remote.name}`);
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -164,6 +192,12 @@ export function ProxmoxRemotesPage() {
onDelete={(remote) => { onDelete={(remote) => {
setRemovingRemote(remote as RemoteInfo | null); setRemovingRemote(remote as RemoteInfo | null);
}} }}
onConnect={(remote) => {
void handleConnectRemote(remote as RemoteInfo);
}}
onDisconnect={(remote) => {
handleDisconnectRemote(remote as RemoteInfo);
}}
/> />
{showAddDialog && ( {showAddDialog && (