Merge pull request 'fix: Proxmox v1.2.2 — client retrieval, AI message ordering, Remotes UX, Ceph false positive' (#124) from fix/proxmox-v1.2.2-consolidated into beta
Some checks failed
Release Beta / autotag (push) Successful in 10s
Release Beta / changelog (push) Successful in 1m35s
Test / frontend-tests (push) Successful in 1m45s
Test / frontend-typecheck (push) Successful in 1m57s
Release Beta / build-linux-amd64 (push) Successful in 12m7s
Release Beta / build-windows-amd64 (push) Successful in 12m28s
Release Beta / build-linux-arm64 (push) Successful in 13m48s
Release Beta / build-macos-arm64 (push) Failing after 21m38s
Test / rust-fmt-check (push) Successful in 23m18s
Test / rust-clippy (push) Successful in 24m34s
Test / rust-tests (push) Successful in 26m42s
Some checks failed
Release Beta / autotag (push) Successful in 10s
Release Beta / changelog (push) Successful in 1m35s
Test / frontend-tests (push) Successful in 1m45s
Test / frontend-typecheck (push) Successful in 1m57s
Release Beta / build-linux-amd64 (push) Successful in 12m7s
Release Beta / build-windows-amd64 (push) Successful in 12m28s
Release Beta / build-linux-arm64 (push) Successful in 13m48s
Release Beta / build-macos-arm64 (push) Failing after 21m38s
Test / rust-fmt-check (push) Successful in 23m18s
Test / rust-clippy (push) Successful in 24m34s
Test / rust-tests (push) Successful in 26m42s
Reviewed-on: #124
This commit is contained in:
commit
cb770661d7
@ -351,9 +351,11 @@ pub async fn chat_message(
|
||||
let agent_registry = create_agent_registry();
|
||||
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();
|
||||
|
||||
// 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 {
|
||||
messages.push(Message {
|
||||
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 !prompt.is_empty() {
|
||||
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
|
||||
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")
|
||||
};
|
||||
|
||||
// 3. Tool-calling system messages — must come BEFORE history so all system messages are contiguous
|
||||
if tools.is_some() && is_openai_compatible {
|
||||
messages.push(Message {
|
||||
role: "system".into(),
|
||||
@ -435,7 +416,6 @@ pub async fn chat_message(
|
||||
tool_calls: None,
|
||||
});
|
||||
|
||||
// Add iteration budget awareness
|
||||
messages.push(Message {
|
||||
role: "system".into(),
|
||||
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 {
|
||||
role: "user".into(),
|
||||
content: full_message.clone(),
|
||||
@ -471,7 +479,7 @@ pub async fn chat_message(
|
||||
// Warn AI when approaching limit
|
||||
if iteration == MAX_TOOL_ITERATIONS - 2 {
|
||||
messages.push(Message {
|
||||
role: "system".into(),
|
||||
role: "user".into(),
|
||||
content: format!(
|
||||
"WARNING: You are on iteration {iteration}/{MAX_TOOL_ITERATIONS} (2 rounds remaining). \
|
||||
You MUST provide your final answer in the NEXT round. \
|
||||
@ -490,7 +498,7 @@ pub async fn chat_message(
|
||||
// Add final instruction
|
||||
let mut final_messages = sanitized_messages;
|
||||
final_messages.push(Message {
|
||||
role: "system".into(),
|
||||
role: "user".into(),
|
||||
content: format!(
|
||||
"CRITICAL: Tool iteration limit reached ({iteration}/{MAX_TOOL_ITERATIONS}). \
|
||||
TOOLS ARE NOW DISABLED. \
|
||||
|
||||
@ -232,16 +232,181 @@ pub async fn get_proxmox_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;
|
||||
// Re-check under write lock: a concurrent task may have already created a client
|
||||
// for this cluster between our read-check and here, so prefer the existing one.
|
||||
if let Some(existing) = clusters.get(cluster_id) {
|
||||
return Ok(existing.clone());
|
||||
}
|
||||
clusters.insert(cluster_id.to_string(), client_arc.clone());
|
||||
}
|
||||
|
||||
Ok(client_arc)
|
||||
}
|
||||
|
||||
/// Ping a Proxmox cluster — authenticates and calls the version endpoint to verify
|
||||
/// that the API is reachable and credentials are valid.
|
||||
#[tauri::command]
|
||||
pub async fn ping_proxmox_cluster(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
client_guard
|
||||
.get::<serde_json::Value>("version", client_guard.ticket.as_deref())
|
||||
.await
|
||||
.map_err(|e| format!("Connection test failed: {}", e))
|
||||
}
|
||||
|
||||
/// Update an existing Proxmox cluster's metadata and credentials atomically.
|
||||
/// Unlike the remove-then-add pattern this is a single SQL UPDATE so there is
|
||||
/// no window where the record is missing.
|
||||
#[tauri::command]
|
||||
pub async fn update_proxmox_cluster(
|
||||
id: String,
|
||||
name: String,
|
||||
cluster_type: ClusterType,
|
||||
connection: ClusterConnection,
|
||||
username: String,
|
||||
password: &str,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ClusterInfo, String> {
|
||||
let credentials = serde_json::json!({ "password": password, "username": username });
|
||||
let encrypted_credentials = crate::integrations::auth::encrypt_token(
|
||||
&serde_json::to_string(&credentials).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| format!("Failed to encrypt credentials: {}", e))?;
|
||||
|
||||
let updated_at = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
|
||||
{
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock database: {}", e))?;
|
||||
|
||||
let rows = db
|
||||
.execute(
|
||||
"UPDATE proxmox_clusters \
|
||||
SET name=?1, cluster_type=?2, url=?3, port=?4, username=?5, \
|
||||
encrypted_credentials=?6, updated_at=?7 \
|
||||
WHERE id=?8",
|
||||
rusqlite::params![
|
||||
name,
|
||||
match cluster_type {
|
||||
ClusterType::VE => "ve",
|
||||
ClusterType::PBS => "pbs",
|
||||
},
|
||||
connection.url,
|
||||
connection.port,
|
||||
username,
|
||||
encrypted_credentials,
|
||||
updated_at,
|
||||
id,
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update cluster: {}", e))?;
|
||||
|
||||
if rows == 0 {
|
||||
return Err(format!("Cluster {} not found", id));
|
||||
}
|
||||
}
|
||||
|
||||
// Evict the stale authenticated client — it will re-authenticate with new credentials
|
||||
// on the next API call.
|
||||
{
|
||||
let mut clusters = state.proxmox_clusters.lock().await;
|
||||
clusters.remove(&id);
|
||||
}
|
||||
|
||||
Ok(ClusterInfo {
|
||||
id,
|
||||
name,
|
||||
cluster_type,
|
||||
url: connection.url,
|
||||
port: connection.port,
|
||||
username,
|
||||
created_at: String::new(),
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// List all Proxmox VMs
|
||||
#[tauri::command]
|
||||
pub async fn list_proxmox_vms(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let vms =
|
||||
@ -266,10 +431,7 @@ pub async fn get_proxmox_vm(
|
||||
vm_id: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let vm = crate::proxmox::vm::get_vm(
|
||||
@ -292,10 +454,7 @@ pub async fn start_proxmox_vm(
|
||||
vm_id: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::vm::start_vm(
|
||||
@ -316,10 +475,7 @@ pub async fn stop_proxmox_vm(
|
||||
vm_id: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::vm::stop_vm(
|
||||
@ -340,10 +496,7 @@ pub async fn reboot_proxmox_vm(
|
||||
vm_id: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::vm::reboot_vm(
|
||||
@ -364,10 +517,7 @@ pub async fn shutdown_proxmox_vm(
|
||||
vm_id: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::vm::shutdown_vm(
|
||||
@ -387,10 +537,7 @@ pub async fn list_proxmox_backup_jobs(
|
||||
node: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let jobs = crate::proxmox::backup::list_backup_jobs(
|
||||
@ -417,10 +564,7 @@ pub async fn list_proxmox_datastores(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let datastores = crate::proxmox::backup::list_datastores(
|
||||
@ -448,10 +592,7 @@ pub async fn trigger_proxmox_backup_job(
|
||||
job_id: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::backup::trigger_backup_job(
|
||||
@ -470,10 +611,7 @@ pub async fn list_ceph_pools(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let pools = crate::proxmox::ceph::list_pools(
|
||||
@ -499,10 +637,7 @@ pub async fn list_ceph_osd(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let osds = crate::proxmox::ceph::list_osds(
|
||||
@ -528,10 +663,7 @@ pub async fn get_ceph_health(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let health = crate::proxmox::ceph::get_ceph_health(
|
||||
@ -552,10 +684,7 @@ pub async fn list_auth_realms(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let realms = crate::proxmox::auth_realm::list_auth_realms(
|
||||
@ -590,10 +719,7 @@ pub async fn add_ldap_realm(
|
||||
certificate: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let config = crate::proxmox::auth_realm::LdapRealmConfig {
|
||||
@ -635,10 +761,7 @@ pub async fn add_ad_realm(
|
||||
certificate: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let config = crate::proxmox::auth_realm::AdRealmConfig {
|
||||
@ -677,10 +800,7 @@ pub async fn add_openid_realm(
|
||||
mapping: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let config = crate::proxmox::auth_realm::OpenidRealmConfig {
|
||||
@ -708,10 +828,7 @@ pub async fn list_acme_accounts(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let accounts = crate::proxmox::acme::list_acme_accounts(
|
||||
@ -737,10 +854,7 @@ pub async fn register_acme_account(
|
||||
terms_of_service_agreed: bool,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let account = crate::proxmox::acme::register_acme_account(
|
||||
@ -762,10 +876,7 @@ pub async fn get_acme_challenges(
|
||||
domain: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let challenges = crate::proxmox::acme::get_acme_challenges(
|
||||
@ -793,10 +904,7 @@ pub async fn list_apt_updates(
|
||||
node: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let updates = crate::proxmox::apt::list_apt_updates(
|
||||
@ -822,10 +930,7 @@ pub async fn update_apt_repos(
|
||||
node: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::apt::update_apt_repos(
|
||||
@ -844,10 +949,7 @@ pub async fn list_apt_repositories(
|
||||
node: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let repos = crate::proxmox::apt::list_apt_repositories(
|
||||
@ -873,10 +975,7 @@ pub async fn get_shell_ticket(
|
||||
remote: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let ticket = crate::proxmox::shell::get_shell_ticket(
|
||||
@ -896,10 +995,7 @@ pub async fn list_views(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let views = crate::proxmox::views::list_views(
|
||||
@ -930,10 +1026,7 @@ pub async fn add_view(
|
||||
enabled: bool,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let widgets: Vec<crate::proxmox::views::Widget> = widgets
|
||||
@ -976,10 +1069,7 @@ pub async fn update_view(
|
||||
enabled: bool,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let widgets: Vec<crate::proxmox::views::Widget> = widgets
|
||||
@ -1017,10 +1107,7 @@ pub async fn delete_view(
|
||||
view_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::views::delete_view(
|
||||
@ -1038,10 +1125,7 @@ pub async fn list_certificates(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let certs = crate::proxmox::certificates::list_certificates(
|
||||
@ -1068,10 +1152,7 @@ pub async fn upload_certificate(
|
||||
name: Option<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let cert = crate::proxmox::certificates::upload_certificate(
|
||||
@ -1094,10 +1175,7 @@ pub async fn get_certificate(
|
||||
cert_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let cert = crate::proxmox::certificates::get_certificate(
|
||||
@ -1121,10 +1199,7 @@ pub async fn list_firewall_rules(
|
||||
node: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let rules = crate::proxmox::firewall::list_firewall_rules(
|
||||
@ -1157,10 +1232,7 @@ pub async fn add_firewall_rule(
|
||||
enabled: bool,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let rule = crate::proxmox::firewall::FirewallRule {
|
||||
@ -1191,10 +1263,7 @@ pub async fn delete_firewall_rule(
|
||||
rule_num: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::firewall::delete_rule(
|
||||
@ -1214,10 +1283,7 @@ pub async fn list_sdn_controllers(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let controllers = crate::proxmox::sdn::list_evpn_zones(
|
||||
@ -1243,10 +1309,7 @@ pub async fn list_sdn_vnets(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let vnets = crate::proxmox::sdn::list_vnets(
|
||||
@ -1270,10 +1333,7 @@ pub async fn list_sdn_zones(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let zones = crate::proxmox::sdn::list_evpn_zones(
|
||||
@ -1300,10 +1360,7 @@ pub async fn list_ceph_clusters(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let ceph_clusters = crate::proxmox::ceph_cluster::list_ceph_clusters(
|
||||
@ -1328,10 +1385,7 @@ pub async fn get_ceph_cluster_status(
|
||||
ceph_cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let status = crate::proxmox::ceph_cluster::get_ceph_cluster_status(
|
||||
@ -1358,10 +1412,7 @@ pub async fn migrate_vm(
|
||||
target_cluster: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let task = crate::proxmox::migration::migrate_vm(
|
||||
@ -1385,10 +1436,7 @@ pub async fn list_migration_status(
|
||||
node: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let tasks = crate::proxmox::migration::list_migration_status(
|
||||
@ -1414,10 +1462,7 @@ pub async fn list_updates(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let updates = crate::proxmox::updates_ext::list_updates_all_remotes(
|
||||
@ -1438,10 +1483,7 @@ pub async fn list_updates(
|
||||
/// Refresh updates
|
||||
#[tauri::command]
|
||||
pub async fn refresh_updates(cluster_id: String, state: State<'_, AppState>) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::updates_ext::refresh_updates_all(
|
||||
@ -1459,10 +1501,7 @@ pub async fn install_updates(
|
||||
packages: Vec<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let package_refs: Vec<&str> = packages.iter().map(|s| s.as_str()).collect();
|
||||
@ -1483,10 +1522,7 @@ pub async fn list_tasks(
|
||||
node: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let tasks = crate::proxmox::tasks::list_tasks(
|
||||
@ -1513,10 +1549,7 @@ pub async fn get_task_status(
|
||||
task_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let task = crate::proxmox::tasks::get_task_status(
|
||||
@ -1539,10 +1572,7 @@ pub async fn stop_task(
|
||||
task_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::tasks::stop_task(
|
||||
@ -1564,10 +1594,7 @@ pub async fn get_metrics_summary(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let nodes = crate::proxmox::metrics::list_nodes(
|
||||
@ -1592,10 +1619,7 @@ pub async fn list_metric_collections(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let nodes = crate::proxmox::metrics::list_nodes(
|
||||
@ -1621,10 +1645,7 @@ pub async fn list_ha_groups(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let groups = crate::proxmox::ha::list_ha_groups(
|
||||
@ -1650,10 +1671,7 @@ pub async fn create_ha_group(
|
||||
max_relocate: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::create_ha_group(
|
||||
@ -1678,10 +1696,7 @@ pub async fn update_ha_group(
|
||||
max_relocate: u32,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::update_ha_group(
|
||||
@ -1703,10 +1718,7 @@ pub async fn delete_ha_group(
|
||||
group: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::delete_ha_group(
|
||||
@ -1724,10 +1736,7 @@ pub async fn list_ha_resources(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let resources = crate::proxmox::ha::list_ha_resources(
|
||||
@ -1750,10 +1759,7 @@ pub async fn enable_ha_resource(
|
||||
resource: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::ha::enable_ha_resource(
|
||||
@ -1773,10 +1779,7 @@ pub async fn list_acls(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "access/acl";
|
||||
@ -1798,10 +1801,7 @@ pub async fn list_users(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "access/users";
|
||||
@ -1823,10 +1823,7 @@ pub async fn list_realms(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let realms = crate::proxmox::auth_realm::list_auth_realms(
|
||||
@ -1850,10 +1847,7 @@ pub async fn get_cluster_notes(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "cluster/config";
|
||||
@ -1877,10 +1871,7 @@ pub async fn update_cluster_notes(
|
||||
notes: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "cluster/config";
|
||||
@ -1906,10 +1897,7 @@ pub async fn search_proxmox_resources(
|
||||
query: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = format!("cluster/resources?type=vm&search={}", query);
|
||||
@ -1934,10 +1922,7 @@ pub async fn get_node_status(
|
||||
node_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = format!("nodes/{}/status", node_id);
|
||||
@ -1962,10 +1947,7 @@ pub async fn get_syslog(
|
||||
limit: Option<u32>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let limit_val = limit.unwrap_or(500);
|
||||
@ -1991,10 +1973,7 @@ pub async fn list_network_interfaces(
|
||||
node_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = format!("nodes/{}/network", node_id);
|
||||
@ -2018,10 +1997,7 @@ pub async fn list_cluster_views(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let views = crate::proxmox::views::list_views(
|
||||
@ -2045,10 +2021,7 @@ pub async fn create_cluster_view(
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let view = crate::proxmox::views::DashboardView {
|
||||
@ -2078,10 +2051,7 @@ pub async fn delete_cluster_view(
|
||||
view_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
crate::proxmox::views::delete_view(
|
||||
@ -2101,10 +2071,7 @@ pub async fn get_subscription_status(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "nodes/localhost/subscription";
|
||||
@ -2128,10 +2095,7 @@ pub async fn list_cluster_tasks(
|
||||
limit: Option<u32>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let limit_val = limit.unwrap_or(50);
|
||||
@ -2154,10 +2118,7 @@ pub async fn list_proxmox_containers(
|
||||
cluster_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let clusters = state.proxmox_clusters.lock().await;
|
||||
let client = clusters
|
||||
.get(&cluster_id)
|
||||
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
|
||||
let client = get_proxmox_client_for_cluster(&cluster_id, &state).await?;
|
||||
let client_guard = client.lock().await;
|
||||
|
||||
let path = "cluster/resources?type=lxc";
|
||||
@ -2243,4 +2204,41 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_proxmox_cluster_not_found_error() {
|
||||
let err = format!("Cluster {} not found", "missing-id");
|
||||
assert_eq!(err, "Cluster missing-id not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_proxmox_cluster_rows_zero_means_not_found() {
|
||||
let rows: usize = 0;
|
||||
let result: Result<(), String> = if rows == 0 {
|
||||
Err(format!("Cluster {} not found", "ghost-id"))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("ghost-id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_proxmox_cluster_rows_nonzero_succeeds() {
|
||||
let rows: usize = 1;
|
||||
let result: Result<(), String> = if rows == 0 {
|
||||
Err(format!("Cluster {} not found", "real-id"))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ping_proxmox_cluster_error_message_format() {
|
||||
let raw = anyhow::anyhow!("connection refused");
|
||||
let msg = format!("Connection test failed: {}", raw);
|
||||
assert!(msg.starts_with("Connection test failed:"));
|
||||
assert!(msg.contains("connection refused"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,6 +225,8 @@ pub fn run() {
|
||||
// Proxmox - Existing
|
||||
commands::proxmox::add_proxmox_cluster,
|
||||
commands::proxmox::remove_proxmox_cluster,
|
||||
commands::proxmox::update_proxmox_cluster,
|
||||
commands::proxmox::ping_proxmox_cluster,
|
||||
commands::proxmox::list_proxmox_clusters,
|
||||
commands::proxmox::get_proxmox_cluster,
|
||||
commands::proxmox::list_proxmox_vms,
|
||||
|
||||
@ -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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/index';
|
||||
import { Button } from '@/components/ui/index';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { MoreHorizontal, Plug, PlugZap } from 'lucide-react';
|
||||
|
||||
interface RemoteInfo {
|
||||
id: string;
|
||||
@ -25,6 +25,77 @@ interface RemotesListProps {
|
||||
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(() => {
|
||||
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);
|
||||
}, []);
|
||||
|
||||
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({
|
||||
remotes,
|
||||
onRefresh,
|
||||
@ -100,44 +171,31 @@ export function RemotesList({
|
||||
</TableCell>
|
||||
<TableCell>{remote.lastConnected || '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
onClick={() => onEdit?.(remote)}
|
||||
title="Edit"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">✏️</span>
|
||||
</button>
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{remote.status === 'connected' ? (
|
||||
<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)}
|
||||
title="Disconnect"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">🔌</span>
|
||||
<PlugZap className="h-4 w-4" />
|
||||
</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)}
|
||||
title="Connect"
|
||||
title="Test connection"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">🔌</span>
|
||||
<Plug className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-red-100 hover:text-red-600"
|
||||
onClick={() => onDelete?.(remote)}
|
||||
title="Delete"
|
||||
>
|
||||
<span className="h-4 w-4 text-xs">🗑️</span>
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
title="More"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
<ActionsMenu
|
||||
remote={remote}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onConnect={onConnect}
|
||||
onDisconnect={onDisconnect}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@ -40,6 +40,36 @@ export async function removeProxmoxCluster(id: string): Promise<void> {
|
||||
await invoke("remove_proxmox_cluster", { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing Proxmox cluster's metadata and credentials atomically.
|
||||
* Uses a single SQL UPDATE so there is no window where the record is missing.
|
||||
*/
|
||||
export async function updateProxmoxCluster(
|
||||
id: string,
|
||||
name: string,
|
||||
clusterType: ClusterType,
|
||||
connection: { url: string; port: number },
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<ClusterInfo> {
|
||||
return await invoke<ClusterInfo>("update_proxmox_cluster", {
|
||||
id,
|
||||
name,
|
||||
clusterType,
|
||||
connection,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping a Proxmox cluster — authenticates and calls the version endpoint to verify
|
||||
* the API is reachable and credentials are valid.
|
||||
*/
|
||||
export async function pingProxmoxCluster(clusterId: string): Promise<any> {
|
||||
return await invoke<any>("ping_proxmox_cluster", { clusterId });
|
||||
}
|
||||
|
||||
/**
|
||||
* List all Proxmox clusters
|
||||
*/
|
||||
|
||||
@ -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 { Button } from '@/components/ui/index';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { PoolList, OSDList, CephHealthWidget, MonitorList } from '@/components/Proxmox';
|
||||
import { listProxmoxClusters, listCephPools, listCephOsd, getCephHealth } from '@/lib/proxmoxClient';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={loading}>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
@ -26,9 +127,11 @@ export function ProxmoxCephPage() {
|
||||
<CardTitle>Ceph Health</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CephHealthWidget
|
||||
health={{ status: 'HEALTH_OK', summary: 'Cluster healthy', details: [] }}
|
||||
/>
|
||||
{health ? (
|
||||
<CephHealthWidget health={health} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Loading health data...</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -40,8 +143,8 @@ export function ProxmoxCephPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PoolList
|
||||
pools={[]}
|
||||
onRefresh={() => {}}
|
||||
pools={pools}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -52,8 +155,8 @@ export function ProxmoxCephPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OSDList
|
||||
osds={[]}
|
||||
onRefresh={() => {}}
|
||||
osds={osds}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -66,7 +169,7 @@ export function ProxmoxCephPage() {
|
||||
<CardContent>
|
||||
<MonitorList
|
||||
monitors={[]}
|
||||
onRefresh={() => {}}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -6,7 +6,7 @@ import { AddRemoteForm } from '@/components/Proxmox';
|
||||
import { EditRemoteForm } from '@/components/Proxmox';
|
||||
import { RemoveRemoteDialog } from '@/components/Proxmox';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/index';
|
||||
import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster } from '@/lib/proxmoxClient';
|
||||
import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster, updateProxmoxCluster, pingProxmoxCluster } from '@/lib/proxmoxClient';
|
||||
import { ClusterType } from '@/lib/domain';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@ -35,7 +35,7 @@ export function ProxmoxRemotesPage() {
|
||||
url: c.url,
|
||||
username: c.username,
|
||||
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);
|
||||
} catch (err) {
|
||||
@ -102,11 +102,7 @@ export function ProxmoxRemotesPage() {
|
||||
const clusterType = config.type === 'pve' ? 've' : 'pbs';
|
||||
const { hostname, port } = parseRemoteUrl(config.url, config.type);
|
||||
|
||||
// Edit operation requires remove-then-add since backend doesn't support update.
|
||||
// If add fails after remove, the remote will be lost - this is a known limitation
|
||||
// until backend supports atomic update operations.
|
||||
await removeProxmoxCluster(config.id);
|
||||
await addProxmoxCluster(
|
||||
await updateProxmoxCluster(
|
||||
config.id,
|
||||
config.name,
|
||||
clusterType as ClusterType,
|
||||
@ -136,6 +132,30 @@ export function ProxmoxRemotesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectRemote = async (remote: RemoteInfo) => {
|
||||
try {
|
||||
toast.info(`Testing connection to ${remote.name}...`);
|
||||
await pingProxmoxCluster(remote.id);
|
||||
toast.success(`Connected to ${remote.name}`);
|
||||
setRemotes((prev) =>
|
||||
prev.map((r) => (r.id === remote.id ? { ...r, status: 'connected' } : r))
|
||||
);
|
||||
} 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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -164,6 +184,12 @@ export function ProxmoxRemotesPage() {
|
||||
onDelete={(remote) => {
|
||||
setRemovingRemote(remote as RemoteInfo | null);
|
||||
}}
|
||||
onConnect={(remote) => {
|
||||
void handleConnectRemote(remote as RemoteInfo);
|
||||
}}
|
||||
onDisconnect={(remote) => {
|
||||
handleDisconnectRemote(remote as RemoteInfo);
|
||||
}}
|
||||
/>
|
||||
|
||||
{showAddDialog && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user