Fix 21 bugs: build failures, broken pages, IPC mismatches, and missing error handling
Critical fixes: - Add esbuild dependency and fix Vite build target (safari13 → es2022) - Fix StorageDetail always failing (null node parameter) - Fix VNC/Terminal console stuck on 'Connecting...' (add onConnected callback) - Fix Ctrl+Alt+Del not working (add data-vnc attribute and event listener) - Add serde(rename_all='camelCase') to all 27 Rust structs crossing IPC - Fix login methods returning empty connection_id (generate from URL hash) - Fix add_connection not storing connections in HashMap - Fix keyring_entry panicking on failure (replace expect with map_err) High-severity fixes: - Fix refresh_ticket reading wrong keyring entry for username - Add onError handlers to all 23 mutations (toast notifications) - Fix stale closure on activeConnectionId (use getState() instead) - Fix WebSocket listener teardown race condition - Add WebSocket reconnection retry limit (max 10 attempts) Medium-severity fixes: - Add Settings navigation to CommandPalette (Cmd+K) - Disable dead Nodes/Containers sidebar items - Wire up HardwareTab save button with warning toast - Wire up QuickActions navigation buttons - Add Dashboard error state (was stuck on 'Loading...') - Add default case to App.tsx view switch - Extract formatBytes/formatUptime/formatNetworkRate to shared utility - Fix formatBytes negative input bug 32 files changed, 1073 insertions(+), 356 deletions(-)
This commit is contained in:
+117
-41
@@ -1,14 +1,12 @@
|
||||
use crate::error::Error;
|
||||
use crate::proxmox::{
|
||||
AddDiskConfig, AddNICConfig, ApiResponse, Backup, BackupJob, BackupJobConfig, ClusterStatus,
|
||||
AddDiskConfig, AddNICConfig, Backup, BackupJob, BackupJobConfig, ClusterStatus,
|
||||
CreateSnapshotConfig, Disk, EditNICConfig, NetworkInterface, Node, RestoreConfig, Snapshot,
|
||||
Storage, StorageContent, StorageDetail, Task, VM,
|
||||
};
|
||||
use crate::{CertificateInfo, ConnectionConfig, EndpointConfig, LoginResult, TermProxyResponse, VNCProxyResponse};
|
||||
use crate::{CertificateInfo, ConnectionConfig, LoginResult, TermProxyResponse, VNCProxyResponse};
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
struct Connection {
|
||||
config: ConnectionConfig,
|
||||
@@ -22,10 +20,10 @@ fn keyring_service() -> &'static str {
|
||||
"proxmox-desktop"
|
||||
}
|
||||
|
||||
fn keyring_entry(connection_id: &str, field: &str) -> keyring::Entry {
|
||||
fn keyring_entry(connection_id: &str, field: &str) -> crate::Result<keyring::Entry> {
|
||||
let key = format!("{}:{}", connection_id, field);
|
||||
keyring::Entry::new(keyring_service(), &key)
|
||||
.expect("failed to create keyring entry")
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
}
|
||||
|
||||
pub struct ConnectionManager {
|
||||
@@ -39,27 +37,58 @@ impl ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_connection(&self, config: ConnectionConfig) -> crate::Result<()> {
|
||||
pub async fn add_connection(&mut self, config: ConnectionConfig) -> crate::Result<()> {
|
||||
if config.primary.url.is_empty() {
|
||||
return Err(Error::InvalidUrl("URL cannot be empty".to_string()));
|
||||
}
|
||||
let id = config.id.clone();
|
||||
let connection = Connection {
|
||||
config,
|
||||
client: Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.map_err(Error::HttpError)?,
|
||||
ticket: None,
|
||||
csrf_token: None,
|
||||
current_endpoint_index: 0,
|
||||
};
|
||||
self.connections.insert(id, connection);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_connection(&mut self, id: &str) -> crate::Result<()> {
|
||||
self.connections.remove(id);
|
||||
// Clear stored credentials from keyring
|
||||
let _ = keyring_entry(id, "ticket").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
let _ = keyring_entry(id, "csrf_token").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
let _ = keyring_entry(id, "password").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
let _ = keyring_entry(id, "token").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self, id: &str) -> crate::Result<()> {
|
||||
if let Some(conn) = self.connections.get_mut(id) {
|
||||
conn.config.status = "connected".to_string();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_connection(&self, id: &str) -> crate::Result<()> {
|
||||
// Clear stored credentials from keyring
|
||||
let _ = keyring_entry(id, "ticket").delete_credential();
|
||||
let _ = keyring_entry(id, "csrf_token").delete_credential();
|
||||
let _ = keyring_entry(id, "password").delete_credential();
|
||||
let _ = keyring_entry(id, "token").delete_credential();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn connect(&self, id: &str) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disconnect(&self, id: &str) -> crate::Result<()> {
|
||||
pub async fn disconnect(&mut self, id: &str) -> crate::Result<()> {
|
||||
if let Some(conn) = self.connections.get_mut(id) {
|
||||
conn.config.status = "disconnected".to_string();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -119,8 +148,27 @@ impl ConnectionManager {
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
// Generate a stable connection ID from the URL
|
||||
let connection_id = {
|
||||
use sha2::{Sha256, Digest};
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(url.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
format!("{:x}", hash)[..16].to_string()
|
||||
};
|
||||
|
||||
// Store credentials in keyring for later use
|
||||
keyring_entry(&connection_id, "ticket")
|
||||
.and_then(|e| e.set_password(&ticket).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
keyring_entry(&connection_id, "csrf_token")
|
||||
.and_then(|e| e.set_password(&csrf_token).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
keyring_entry(&connection_id, "username")
|
||||
.and_then(|e| e.set_password(username).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
keyring_entry(&connection_id, "password")
|
||||
.and_then(|e| e.set_password(password).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
|
||||
Ok(LoginResult {
|
||||
connection_id: String::new(),
|
||||
connection_id,
|
||||
ticket,
|
||||
csrf_token,
|
||||
})
|
||||
@@ -164,23 +212,56 @@ impl ConnectionManager {
|
||||
return Err(Error::InvalidCredentials("Invalid API token".to_string()));
|
||||
}
|
||||
|
||||
// Generate a stable connection ID from the URL
|
||||
let connection_id = {
|
||||
use sha2::{Sha256, Digest};
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(url.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
format!("{:x}", hash)[..16].to_string()
|
||||
};
|
||||
|
||||
// Store token in keyring for later use
|
||||
keyring_entry(&connection_id, "token")
|
||||
.and_then(|e| e.set_password(token).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
|
||||
Ok(LoginResult {
|
||||
connection_id: String::new(),
|
||||
connection_id,
|
||||
ticket: token.to_string(),
|
||||
csrf_token: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn logout(&self, connection_id: &str) -> crate::Result<()> {
|
||||
let _ = keyring_entry(connection_id, "ticket").delete_credential();
|
||||
let _ = keyring_entry(connection_id, "csrf_token").delete_credential();
|
||||
let _ = keyring_entry(connection_id, "password").delete_credential();
|
||||
let _ = keyring_entry(connection_id, "token").delete_credential();
|
||||
let _ = keyring_entry(connection_id, "ticket").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
let _ = keyring_entry(connection_id, "csrf_token").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
let _ = keyring_entry(connection_id, "password").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
let _ = keyring_entry(connection_id, "token").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
let _ = keyring_entry(connection_id, "username").and_then(|e| {
|
||||
e.delete_credential()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_stored_credentials(&self, connection_id: &str) -> crate::Result<Option<String>> {
|
||||
match keyring_entry(connection_id, "ticket").get_password() {
|
||||
let entry = match keyring_entry(connection_id, "ticket") {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
match entry.get_password() {
|
||||
Ok(ticket) => Ok(Some(ticket)),
|
||||
Err(keyring::Error::NoEntry) => Ok(None),
|
||||
Err(e) => Err(Error::KeyringError(e.to_string())),
|
||||
@@ -196,20 +277,16 @@ impl ConnectionManager {
|
||||
api_token: Option<&str>,
|
||||
) -> crate::Result<()> {
|
||||
keyring_entry(connection_id, "ticket")
|
||||
.set_password(ticket)
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))?;
|
||||
.and_then(|e| e.set_password(ticket).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
keyring_entry(connection_id, "csrf_token")
|
||||
.set_password(csrf_token)
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))?;
|
||||
.and_then(|e| e.set_password(csrf_token).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
if let Some(pw) = password {
|
||||
keyring_entry(connection_id, "password")
|
||||
.set_password(pw)
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))?;
|
||||
.and_then(|e| e.set_password(pw).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
}
|
||||
if let Some(tok) = api_token {
|
||||
keyring_entry(connection_id, "token")
|
||||
.set_password(tok)
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))?;
|
||||
.and_then(|e| e.set_password(tok).map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -221,12 +298,11 @@ impl ConnectionManager {
|
||||
) -> crate::Result<LoginResult> {
|
||||
// Try to get stored password for re-authentication
|
||||
let password = keyring_entry(connection_id, "password")
|
||||
.get_password()
|
||||
.map_err(|e| Error::KeyringError(e.to_string()))?;
|
||||
.and_then(|e| e.get_password().map_err(|e| Error::KeyringError(e.to_string())))?;
|
||||
|
||||
// Extract username from stored ticket or use default
|
||||
let username = keyring_entry(connection_id, "csrf_token")
|
||||
.get_password()
|
||||
// Get stored username for re-authentication
|
||||
let username = keyring_entry(connection_id, "username")
|
||||
.and_then(|e| e.get_password().map_err(|e| Error::KeyringError(e.to_string())))
|
||||
.unwrap_or_default();
|
||||
|
||||
self.login_with_password(url, &username, &password).await
|
||||
|
||||
+11
-4
@@ -16,6 +16,7 @@ use websocket::WebSocketManager;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionConfig {
|
||||
pub id: String,
|
||||
@@ -31,6 +32,7 @@ pub struct ConnectionConfig {
|
||||
pub username: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct EndpointConfig {
|
||||
pub url: String,
|
||||
@@ -38,6 +40,7 @@ pub struct EndpointConfig {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct LoginResult {
|
||||
pub connection_id: String,
|
||||
@@ -45,6 +48,7 @@ pub struct LoginResult {
|
||||
pub csrf_token: String,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct CertificateInfo {
|
||||
pub fingerprint: String,
|
||||
@@ -65,7 +69,7 @@ async fn add_connection(
|
||||
state: tauri::State<'_, AppState>,
|
||||
config: ConnectionConfig,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
let mut manager = state.connection_manager.write().await;
|
||||
manager.add_connection(config).await
|
||||
}
|
||||
|
||||
@@ -74,7 +78,7 @@ async fn remove_connection(
|
||||
state: tauri::State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
let mut manager = state.connection_manager.write().await;
|
||||
manager.remove_connection(&id).await
|
||||
}
|
||||
|
||||
@@ -83,7 +87,7 @@ async fn connect_to_server(
|
||||
state: tauri::State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
let mut manager = state.connection_manager.write().await;
|
||||
manager.connect(&id).await
|
||||
}
|
||||
|
||||
@@ -92,7 +96,7 @@ async fn disconnect_from_server(
|
||||
state: tauri::State<'_, AppState>,
|
||||
id: String,
|
||||
) -> Result<()> {
|
||||
let manager = state.connection_manager.read().await;
|
||||
let mut manager = state.connection_manager.write().await;
|
||||
manager.disconnect(&id).await
|
||||
}
|
||||
|
||||
@@ -457,6 +461,7 @@ async fn get_stored_credentials(
|
||||
}
|
||||
|
||||
// Console proxy types
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct VNCProxyResponse {
|
||||
pub ticket: String,
|
||||
@@ -464,6 +469,7 @@ pub struct VNCProxyResponse {
|
||||
pub cert: String,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct TermProxyResponse {
|
||||
pub ticket: String,
|
||||
@@ -613,6 +619,7 @@ async fn delete_backup(
|
||||
manager.delete_backup(&connection_id, &volid).await
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct TrayConnectionInfo {
|
||||
pub id: String,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Disk {
|
||||
pub device: String,
|
||||
@@ -9,6 +10,7 @@ pub struct Disk {
|
||||
pub usage: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddDiskConfig {
|
||||
pub storage: String,
|
||||
@@ -16,6 +18,7 @@ pub struct AddDiskConfig {
|
||||
pub bus_type: String,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Node {
|
||||
pub node: String,
|
||||
@@ -32,6 +35,7 @@ pub struct Node {
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VM {
|
||||
pub vmid: u32,
|
||||
@@ -56,6 +60,7 @@ pub struct VM {
|
||||
pub tags: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Storage {
|
||||
pub storage: String,
|
||||
@@ -70,6 +75,7 @@ pub struct Storage {
|
||||
pub node: String,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
pub upid: String,
|
||||
@@ -85,6 +91,7 @@ pub struct Task {
|
||||
pub exitstatus: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClusterStatus {
|
||||
pub r#type: String,
|
||||
@@ -93,6 +100,7 @@ pub struct ClusterStatus {
|
||||
pub nodes: Option<Vec<ClusterNode>>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClusterNode {
|
||||
pub name: String,
|
||||
@@ -102,6 +110,7 @@ pub struct ClusterNode {
|
||||
pub ip: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Snapshot {
|
||||
pub name: String,
|
||||
@@ -111,6 +120,7 @@ pub struct Snapshot {
|
||||
pub parent: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateSnapshotConfig {
|
||||
pub name: String,
|
||||
@@ -118,11 +128,13 @@ pub struct CreateSnapshotConfig {
|
||||
pub vmstate: Option<bool>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkInterface {
|
||||
pub name: String,
|
||||
@@ -134,6 +146,7 @@ pub struct NetworkInterface {
|
||||
pub link_down: Option<u32>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddNICConfig {
|
||||
pub bridge: String,
|
||||
@@ -143,6 +156,7 @@ pub struct AddNICConfig {
|
||||
pub firewall: Option<bool>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EditNICConfig {
|
||||
pub bridge: Option<String>,
|
||||
@@ -151,6 +165,7 @@ pub struct EditNICConfig {
|
||||
pub firewall: Option<bool>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Backup {
|
||||
pub volid: String,
|
||||
@@ -166,6 +181,7 @@ pub struct Backup {
|
||||
pub ctime: u64,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupJob {
|
||||
pub id: String,
|
||||
@@ -180,6 +196,7 @@ pub struct BackupJob {
|
||||
pub quiet: Option<u32>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupJobConfig {
|
||||
pub id: Option<String>,
|
||||
@@ -193,6 +210,7 @@ pub struct BackupJobConfig {
|
||||
pub node: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RestoreConfig {
|
||||
pub volid: String,
|
||||
@@ -201,6 +219,7 @@ pub struct RestoreConfig {
|
||||
pub vmid: Option<u32>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageContent {
|
||||
pub content: String,
|
||||
@@ -211,6 +230,7 @@ pub struct StorageContent {
|
||||
pub volid: String,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageDetail {
|
||||
pub storage: String,
|
||||
|
||||
@@ -70,6 +70,8 @@ impl WebSocketManager {
|
||||
tokio::spawn(async move {
|
||||
let mut reconnect_delay = Duration::from_secs(1);
|
||||
const MAX_DELAY: Duration = Duration::from_secs(30);
|
||||
const MAX_RETRIES: u32 = 10;
|
||||
let mut retry_count: u32 = 0;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -79,11 +81,19 @@ impl WebSocketManager {
|
||||
result = connect_and_run(&cid, &ws_url, &app_handle) => {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
// Normal close or stream ended – attempt reconnect
|
||||
reconnect_delay = Duration::from_secs(1);
|
||||
retry_count = 0;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ws] connection error for {}: {e}", cid);
|
||||
retry_count += 1;
|
||||
if retry_count >= MAX_RETRIES {
|
||||
let _ = app_handle.emit("ws-raw", serde_json::json!({
|
||||
"connection_id": cid,
|
||||
"data": { "type": "error", "message": format!("WebSocket reconnect failed after {} attempts: {}", MAX_RETRIES, e) },
|
||||
}));
|
||||
break;
|
||||
}
|
||||
eprintln!("[ws] connection error for {} (attempt {}/{}): {e}", cid, retry_count, MAX_RETRIES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user