Skip to main content

Mountain/IPC/
WindAirCommands.rs

1//! # Wind-Air Commands - Air Daemon Delegation Layer
2//!
3//! **File Responsibilities:**
4//! This module provides the Tauri IPC commands that enable Wind (the TypeScript
5//! frontend) to delegate background operations to Air (the Rust daemon). All
6//! commands use the gRPC-based AirClient for communication and return
7//! structured DTOs or detailed error messages.
8//!
9//! **Architectural Role in Wind-Mountain-Air Connection:**
10//!
11//! The WindAirCommands module forms the delegation layer that:
12//!
13//! 1. **Bridge to Air Daemon:** Provides a Tauri IPC interface to Air's gRPC
14//!    services
15//! 2. **Background Operations:** Offloads long-running tasks from Wind to Air
16//! 3. **Type Translation:** Converts between Tauri JSON and gRPC protobuf
17//!    messages
18//! 4. **Error Handling:** Translates gRPC errors to user-friendly error
19//!    messages
20//! 5. **Connection Management:** Manages Air client lifecycle and reconnections
21//!
22//! **Three-Tier Architecture:**
23//! ```
24//! Wind (Frontend - TypeScript)
25//!   |
26//!   | Tauri IPC Commands
27//!   v
28//! WindAirCommands (Mountain - Rust)
29//!   |
30//!   | gRPC Calls
31//!   v
32//! AirClient (gRPC Client)
33//!   |
34//!   | Network Communication
35//!   v
36//! Air Daemon (gRPC Server)
37//! ```
38//!
39//! **Available Commands (Tauri IPC):**
40//!
41//! **1. Update Management:**
42//! - `CheckForUpdates` - Check for application updates
43//! - `DownloadUpdate` - Download update package
44//! - `ApplyUpdate` - Apply downloaded update
45//!
46//! **2. File Operations:**
47//! - `DownloadFile` - Download any file from URL
48//!
49//! **3. Authentication:**
50//! - `AuthenticateUser` - Authenticate with various providers
51//!
52//! **4. Indexing & Search:**
53//! - `IndexFiles` - Index directory contents
54//! - `SearchFiles` - Search indexed files
55//!
56//! **5. Monitoring:**
57//! - `GetAirStatus` - Get Air daemon status
58//! - `GetAirMetrics` - Get performance & resource metrics
59//!
60//! **Data Transfer Objects (DTOs):**
61//!
62//! **UpdateInfoDTO:**
63//! ```rust
64//! struct UpdateInfoDTO {
65//! 	update_available:bool,
66//! 	version:String,
67//! 	download_url:String,
68//! 	release_notes:String,
69//! }
70//! ```
71//!
72//! **DownloadResultDTO:**
73//! ```rust
74//! struct DownloadResultDTO {
75//! 	success:bool,
76//! 	file_path:String,
77//! 	file_size:u64,
78//! 	checksum:String,
79//! }
80//! ```
81//!
82//! **AuthResponseDTO:**
83//! ```rust
84//! struct AuthResponseDTO {
85//! 	success:bool,
86//! 	token:String,
87//! 	error:Option<String>,
88//! }
89//! ```
90//!
91//! **IndexResultDTO:**
92//! ```rust
93//! struct IndexResultDTO {
94//! 	success:bool,
95//! 	files_indexed:u32,
96//! 	total_size:u64,
97//! }
98//! ```
99//!
100//! **SearchResultsDTO:**
101//! ```rust
102//! struct SearchResultsDTO {
103//! 	results:Vec<FileResultDTO>,
104//! 	total_results:u32,
105//! }
106//! ```
107//!
108//! **FileResultDTO:**
109//! ```rust
110//! struct FileResultDTO {
111//! 	path:String,
112//! 	size:u64,
113//! 	line:Option<u32>,
114//! 	content:Option<String>,
115//! }
116//! ```
117//!
118//! **AirServiceStatusDTO:**
119//! ```rust
120//! struct AirServiceStatusDTO {
121//! 	version:String,
122//! 	uptime_seconds:u64,
123//! 	total_requests:u64,
124//! 	successful_requests:u64,
125//! 	failed_requests:u64,
126//! 	active_requests:u32,
127//! 	healthy:bool,
128//! }
129//! ```
130//!
131//! **AirMetricsDTO:**
132//! ```rust
133//! struct AirMetricsDTO {
134//! 	memory_usage_mb:f64,
135//! 	cpu_usage_percent:f64,
136//! 	average_response_time:f64,
137//! 	disk_usage_mb:f64,
138//! 	network_usage_mbps:f64,
139//! }
140//! ```
141//!
142//! **Command Registration:**
143//!
144//! All commands are registered with Tauri's invoke_handler:
145//!
146//! ```rust
147//! builder.invoke_handler(tauri::generate_handler![
148//! 	CheckForUpdates,
149//! 	DownloadUpdate,
150//! 	ApplyUpdate,
151//! 	DownloadFile,
152//! 	AuthenticateUser,
153//! 	IndexFiles,
154//! 	SearchFiles,
155//! 	GetAirStatus,
156//! 	GetAirMetrics,
157//! ])
158//! ```
159//!
160//! **Client Connection Management:**
161//!
162//! **AirClientWrapper:**
163//! - Wraps the gRPC AirClient
164//! - Manages reconnections
165//! - Default address: `DEFAULT_AIR_SERVER_ADDRESS`
166//!
167//! **Connection Flow:**
168//! ```rust
169//! // 1. Get Air address from config
170//! let air_address = get_air_address(&app_handle)?;
171//!
172//! // 2. Create or reuse client
173//! let client = get_or_create_air_client(&app_handle, air_address).await?;
174//!
175//! // 3. Call Air's gRPC method
176//! let response = client.CheckForUpdates(request).await?;
177//!
178//! // 4. Check for errors
179//! if !response.error.is_empty() {
180//!     return Err(response.error);
181//! }
182//!
183//! // 5. Convert to DTO
184//! let result = UpdateInfoDTO { ... };
185//! ```
186//!
187//! **Error Handling Strategy:**
188//!
189//! **gRPC Errors:**
190//! - Catch all gRPC errors
191//! - Translate to user-friendly messages
192//! - Include context about what operation failed
193//!
194//! **Response Errors:**
195//! - Check `response.error` field
196//! - Return error instead of DTO if present
197//! - Preserve original error message
198//!
199//! **Client Errors:**
200//! - Connection failures -> "Failed to connect to Air daemon"
201//! - Timeout errors -> "Operation timed out"
202//! - Parse errors -> "Failed to parse response"
203//!
204//! **Usage Examples from Wind:**
205//!
206//! **Check for Updates:**
207//! ```typescript
208//! const updates = await invoke('CheckForUpdates', {
209//!     currentVersion: '1.0.0',
210//!     channel: 'stable'
211//! });
212//!
213//! if (updates.updateAvailable) {
214//!     console.log(`New version: ${updates.version}`);
215//! }
216//! ```
217//!
218//! **Download Update:**
219//! ```typescript
220//! const result = await invoke('DownloadUpdate', {
221//!     url: 'https://example.com/update.zip',
222//!     destination: '/tmp/update.zip',
223//!     checksum: 'abc123...'
224//! });
225//!
226//! if (result.success) {
227//!     console.log(`Downloaded: ${result.filePath}`);
228//! }
229//! ```
230//!
231//! **Authenticate:**
232//! ```typescript
233//! const auth = await invoke('AuthenticateUser', {
234//!     username: '[email protected]',
235//!     password: 'secret',
236//!     provider: 'github'
237//! });
238//!
239//! if (auth.success) {
240//!     localStorage.setItem('token', auth.token);
241//! }
242//! ```
243//!
244//! **Index Files:**
245//! ```typescript
246//! const indexResult = await invoke('IndexFiles', {
247//!     path: '/project',
248//!     patterns: ['*.ts', '*.rs'],
249//!     excludePatterns: ['node_modules', 'target'],
250//!     maxDepth: 10
251//! });
252//!
253//! console.log(`Indexed ${indexResult.filesIndexed} files`);
254//! ```
255//!
256//! **Search Files:**
257//! ```typescript
258//! const searchResults = await invoke('SearchFiles', {
259//!     query: 'TODO:',
260//!     indexId: '/project',
261//!     maxResults: 100
262//! });
263//!
264//! for (const file of searchResults.results) {
265//!     console.log(`${file.path}:${file.line} - ${file.content}`);
266//! }
267//! ```
268//!
269//! **Integration with Other Modules:**
270//!
271//! **TauriIPCServer:**
272//! - Commands registered in same invoke handler
273//! - Both provide Tauri IPC interfaces
274//!
275//! **Configuration:**
276//! - Air address configurable via Mountain settings
277//! - Uses default if not specified
278//!
279//! **StatusReporter:**
280//! - Air status can be reported to Sky
281//! - Metrics collected for monitoring
282//!
283//! **Security Considerations:**
284//!
285//! - Passwords never logged
286//! - Checksums verified for downloads
287//! - File paths validated
288//! - Provider authentication handled securely by Air
289//!
290//! **Performance Considerations:**
291//!
292//! - Client connections are created fresh each call (current implementation)
293//! - Could cache clients for better performance in production
294//! - Large file downloads streamed via Air
295//! - Indexing operations run asynchronously in Air
296
297use serde::{Deserialize, Serialize};
298use tauri::Manager;
299
300// Import Air types from the new AirClient implementation.
301// These provide actual gRPC connectivity to the Air daemon service.
302use crate::Air::AirClient as AirClientModule;
303use crate::Air::AirClient::DEFAULT_AIR_SERVER_ADDRESS;
304use crate::dev_log;
305
306/// Data Transfer Objects for Wind-Air communication
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct UpdateInfoDTO {
310	pub update_available:bool,
311	pub version:String,
312	pub download_url:String,
313	pub release_notes:String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct DownloadResultDTO {
318	pub success:bool,
319	pub file_path:String,
320	pub file_size:u64,
321	pub checksum:String,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct AuthResponseDTO {
326	pub success:bool,
327	pub token:String,
328	pub error:Option<String>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct IndexResultDTO {
333	pub success:bool,
334	pub files_indexed:u32,
335	pub total_size:u64,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct SearchResultsDTO {
340	pub results:Vec<FileResultDTO>,
341	pub total_results:u32,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct FileResultDTO {
346	pub path:String,
347	pub size:u64,
348	pub line:Option<u32>,
349	pub content:Option<String>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct AirServiceStatusDTO {
354	pub version:String,
355	pub uptime_seconds:u64,
356	pub total_requests:u64,
357	pub successful_requests:u64,
358	pub failed_requests:u64,
359	pub active_requests:u32,
360	pub healthy:bool,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct AirMetricsDTO {
365	pub memory_usage_mb:f64,
366	pub cpu_usage_percent:f64,
367	pub average_response_time:f64,
368	pub disk_usage_mb:f64,
369	pub network_usage_mbps:f64,
370}
371
372/// Air Client - Wrapper for the gRPC client connection to Air daemon
373#[derive(Debug, Clone)]
374pub struct AirClientWrapper {
375	client:AirClientModule::AirClient,
376}
377
378impl AirClientWrapper {
379	/// Create a new AirClient connected to the Air daemon
380	pub async fn new(address:String) -> Result<Self, String> {
381		dev_log!("grpc", "[WindAirCommands] Connecting to Air daemon at: {}", address);
382
383		let client = AirClientModule::AirClient::new(&address)
384			.await
385			.map_err(|e| format!("Failed to connect to Air daemon: {:?}", e))?;
386
387		dev_log!("grpc", "[WindAirCommands] Successfully connected to Air daemon");
388		Ok(Self { client })
389	}
390
391	/// Reconnect to Air daemon
392	pub async fn reconnect(&mut self, address:String) -> Result<(), String> {
393		dev_log!("grpc", "[WindAirCommands] Reconnecting to Air daemon at: {}", address);
394
395		let client = AirClientModule::AirClient::new(&address)
396			.await
397			.map_err(|e| format!("Failed to reconnect to Air daemon: {:?}", e))?;
398
399		self.client = client;
400		dev_log!("grpc", "[WindAirCommands] Successfully reconnected to Air daemon");
401		Ok(())
402	}
403}
404
405// ============================================================================
406// Tauri IPC Commands for Wind-Air Communication
407// ============================================================================
408
409/// Command: Check for Updates
410///
411/// Checks if a newer version of the application is available.
412/// Delegates to Air's update checking service.
413///
414/// # Arguments
415/// * `app_handle` - Tauri application handle
416/// * `current_version` - Current application version
417/// * `channel` - Update channel ("stable", "beta", "nightly")
418///
419/// # Returns
420/// `UpdateInfoDTO` with update information or error message
421#[tauri::command]
422pub async fn CheckForUpdates(current_version:Option<String>, channel:Option<String>) -> Result<UpdateInfoDTO, String> {
423	dev_log!(
424		"grpc",
425		"[WindAirCommands] CheckForUpdates called with version: {:?}, channel: {:?}",
426		current_version,
427		channel
428	);
429
430	// Get the Air client from app state or configuration
431	let air_address = get_air_address()?;
432	let client = get_or_create_air_client(air_address).await?;
433
434	// Use the new AirClient API
435	let request_id = uuid::Uuid::new_v4().to_string();
436	let current_version = current_version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
437	let channel = channel.unwrap_or_else(|| "stable".to_string());
438
439	// Delegate to Air via gRPC
440	let update_info = client
441		.check_for_updates(request_id, current_version, channel)
442		.await
443		.map_err(|e| format!("Update check failed: {:?}", e))?;
444
445	let result = UpdateInfoDTO {
446		update_available:update_info.update_available,
447		version:update_info.version,
448		download_url:update_info.download_url,
449		release_notes:update_info.release_notes,
450	};
451
452	dev_log!(
453		"grpc",
454		"[WindAirCommands] Update check completed: available={}",
455		result.update_available
456	);
457	Ok(result)
458}
459
460/// Command: Download Update
461///
462/// Downloads an application update from the specified URL.
463/// Delegates to Air's download service.
464///
465/// # Arguments
466/// * `app_handle` - Tauri application handle
467/// * `url` - URL to download the update from
468/// * `destination` - Local destination path for the download
469/// * `checksum` - Optional SHA256 checksum for verification
470///
471/// # Returns
472/// `DownloadResultDTO` with download status
473#[tauri::command]
474pub async fn DownloadUpdate(
475	url:String,
476	destination:String,
477	checksum:Option<String>,
478) -> Result<DownloadResultDTO, String> {
479	dev_log!("grpc", "[WindAirCommands] DownloadUpdate called: {} -> {}", url, destination);
480
481	let air_address = get_air_address()?;
482	let client = get_or_create_air_client(air_address).await?;
483
484	let request_id = uuid::Uuid::new_v4().to_string();
485
486	// Delegate to Air via gRPC
487	let file_info = client
488		.download_update(
489			request_id,
490			url,
491			destination,
492			checksum.unwrap_or_default(),
493			std::collections::HashMap::new(),
494		)
495		.await
496		.map_err(|e| format!("Update download failed: {:?}", e))?;
497
498	let result = DownloadResultDTO {
499		success:true,
500		file_path:file_info.file_path,
501		file_size:file_info.file_size,
502		checksum:file_info.checksum,
503	};
504
505	dev_log!(
506		"grpc",
507		"[WindAirCommands] Update download completed: success={}",
508		result.success
509	);
510	Ok(result)
511}
512
513/// Command: Apply Update
514///
515/// Applies a downloaded update to the application.
516/// Delegates to Air's update installation service.
517///
518/// # Arguments
519/// * `app_handle` - Tauri application handle
520/// * `update_id` - Identifier of the update to apply
521/// * `update_path` - Path to the update package
522///
523/// # Returns
524/// Success status or error message
525#[tauri::command]
526pub async fn ApplyUpdate(update_id:String, update_path:String) -> Result<bool, String> {
527	dev_log!(
528		"grpc",
529		"[WindAirCommands] ApplyUpdate called: id={}, path={}",
530		update_id,
531		update_path
532	);
533
534	let air_address = get_air_address()?;
535	let client = get_or_create_air_client(air_address).await?;
536
537	let request_id = uuid::Uuid::new_v4().to_string();
538
539	// Apply downloaded updates by sending ApplyUpdateRequest to the Air service.
540	// The Air service handles platform-specific installation (replacing binaries,
541	// restarting the application, cleaning up old versions).
542	client
543		.apply_update(request_id, update_id, update_path)
544		.await
545		.map_err(|e| format!("Update application failed: {:?}", e))?;
546
547	dev_log!("grpc", "[WindAirCommands] Update applied successfully");
548	Ok(true)
549}
550
551/// Command: Download File
552///
553/// Downloads any file from a URL.
554/// Delegates to Air's download service.
555///
556/// # Arguments
557/// * `app_handle` - Tauri application handle
558/// * `url` - URL to download from
559/// * `destination` - Local destination path
560///
561/// # Returns
562/// `DownloadResultDTO` with download status
563#[tauri::command]
564pub async fn DownloadFile(url:String, destination:String) -> Result<DownloadResultDTO, String> {
565	dev_log!("grpc", "[WindAirCommands] DownloadFile called: {} -> {}", url, destination);
566
567	let air_address = get_air_address()?;
568	let client = get_or_create_air_client(air_address).await?;
569
570	let request_id = uuid::Uuid::new_v4().to_string();
571
572	let file_info = client
573		.download_file(request_id, url, destination, String::new(), std::collections::HashMap::new())
574		.await
575		.map_err(|e| format!("File download failed: {:?}", e))?;
576
577	let result = DownloadResultDTO {
578		success:true,
579		file_path:file_info.file_path,
580		file_size:file_info.file_size,
581		checksum:file_info.checksum,
582	};
583
584	dev_log!("grpc", "[WindAirCommands] File download completed");
585	Ok(result)
586}
587
588/// Command: Authenticate User
589///
590/// Authenticates a user with the specified provider.
591/// Delegates to Air's authentication service.
592///
593/// # Arguments
594/// * `app_handle` - Tauri application handle
595/// * `username` - User's username/email
596/// * `password` - User's password (or auth token)
597/// * `provider` - Auth provider ("github", "gitlab", "microsoft", etc.)
598///
599/// # Returns
600/// `AuthResponseDTO` with authentication token
601#[tauri::command]
602pub async fn AuthenticateUser(username:String, password:String, provider:String) -> Result<AuthResponseDTO, String> {
603	dev_log!(
604		"grpc",
605		"[WindAirCommands] AuthenticateUser called: {} via {}",
606		username,
607		provider
608	);
609
610	let air_address = get_air_address()?;
611	let client = get_or_create_air_client(air_address).await?;
612
613	let request_id = uuid::Uuid::new_v4().to_string();
614
615	let token = client
616		.authenticate(request_id, username, password, provider)
617		.await
618		.map_err(|e| format!("Authentication failed: {:?}", e))?;
619
620	let result = AuthResponseDTO { success:true, token, error:None };
621
622	dev_log!("grpc", "[WindAirCommands] Authentication completed: success={}", result.success);
623	Ok(result)
624}
625
626/// Command: Index Files
627///
628/// Initiates file indexing for a directory.
629/// Delegates to Air's file indexing service.
630///
631/// # Arguments
632/// * `app_handle` - Tauri application handle
633/// * `path` - Path to directory to index
634/// * `patterns` - File patterns to include
635/// * `exclude_patterns` - File patterns to exclude
636/// * `max_depth` - Maximum directory depth to traverse
637///
638/// # Returns
639/// `IndexResultDTO` with indexing results
640#[tauri::command]
641pub async fn IndexFiles(
642	path:String,
643	patterns:Vec<String>,
644	exclude_patterns:Option<Vec<String>>,
645	max_depth:Option<u32>,
646) -> Result<IndexResultDTO, String> {
647	dev_log!(
648		"grpc",
649		"[WindAirCommands] IndexFiles called: {} with patterns: {:?}",
650		path,
651		patterns
652	);
653
654	let air_address = get_air_address()?;
655	let client = get_or_create_air_client(air_address).await?;
656
657	let request_id = uuid::Uuid::new_v4().to_string();
658
659	let index_info = client
660		.index_files(
661			request_id,
662			path,
663			patterns,
664			exclude_patterns.unwrap_or_default(),
665			max_depth.unwrap_or(100),
666		)
667		.await
668		.map_err(|e| format!("File indexing failed: {:?}", e))?;
669
670	let result = IndexResultDTO {
671		success:true,
672		files_indexed:index_info.files_indexed,
673		total_size:index_info.total_size,
674	};
675
676	dev_log!(
677		"grpc",
678		"[WindAirCommands] File indexing completed: {} files",
679		result.files_indexed
680	);
681	Ok(result)
682}
683
684/// Command: Search Files
685///
686/// Searches previously indexed files.
687/// Delegates to Air's search service.
688///
689/// # Arguments
690/// * `app_handle` - Tauri application handle
691/// * `query` - Search query string
692/// * `index_id` - Index identifier (or path for backward compatibility)
693/// * `max_results` - Maximum number of results to return
694///
695/// # Returns
696/// `SearchResultsDTO` with matching files
697#[tauri::command]
698pub async fn SearchFiles(
699	query:String,
700	file_patterns:Vec<String>,
701	max_results:Option<u32>,
702) -> Result<SearchResultsDTO, String> {
703	dev_log!(
704		"grpc",
705		"[WindAirCommands] SearchFiles called: query={}, patterns={:?}",
706		query,
707		file_patterns
708	);
709
710	let air_address = get_air_address()?;
711	let client = get_or_create_air_client(air_address).await?;
712
713	let request_id = uuid::Uuid::new_v4().to_string();
714	let max_results_count = max_results.unwrap_or(100);
715
716	let search_results = client
717		.search_files(
718			request_id,
719			query,
720			file_patterns.first().map(|s| s.as_str()).unwrap_or("").to_string(),
721			max_results_count,
722		)
723		.await
724		.map_err(|e| format!("File search failed: {:?}", e))?;
725
726	let results:Vec<FileResultDTO> = search_results
727		.into_iter()
728		.map(|r| {
729			FileResultDTO {
730				path:r.path,
731				size:r.size,
732				line:Some(r.line_number),
733				content:Some(r.match_preview),
734			}
735		})
736		.collect();
737
738	let total_results = results.len() as u32;
739	let result = SearchResultsDTO { results, total_results };
740
741	dev_log!(
742		"grpc",
743		"[WindAirCommands] File search completed: {} results",
744		result.total_results
745	);
746	Ok(result)
747}
748
749/// Command: Get Air Status
750///
751/// Retrieves the current status of the Air daemon.
752/// Delegates to Air's status service.
753///
754/// # Arguments
755/// * `app_handle` - Tauri application handle
756///
757/// # Returns
758/// `AirServiceStatusDTO` with service status information
759#[tauri::command]
760pub async fn GetAirStatus() -> Result<AirServiceStatusDTO, String> {
761	dev_log!("grpc", "[WindAirCommands] GetAirStatus called");
762
763	let air_address = get_air_address()?;
764	let client = get_or_create_air_client(air_address).await?;
765
766	let request_id = uuid::Uuid::new_v4().to_string();
767
768	let status = client
769		.get_status(request_id)
770		.await
771		.map_err(|e| format!("Failed to get Air status: {:?}", e))?;
772
773	// Use the health check RPC to determine service availability
774	let healthy = client.health_check().await.unwrap_or(false);
775
776	let result = AirServiceStatusDTO {
777		version:status.version,
778		uptime_seconds:status.uptime_seconds,
779		total_requests:status.total_requests,
780		successful_requests:status.successful_requests,
781		failed_requests:status.failed_requests,
782		active_requests:status.active_requests,
783		healthy,
784	};
785
786	dev_log!("grpc", "[WindAirCommands] Air status retrieved: healthy={}", result.healthy);
787	Ok(result)
788}
789
790/// Command: Get Air Metrics
791///
792/// Retrieves performance and resource metrics from Air.
793/// Delegates to Air's metrics service.
794///
795/// # Arguments
796/// * `app_handle` - Tauri application handle
797/// * `metric_type` - Type of metrics ("all", "performance", "resources",
798///   "requests")
799///
800/// # Returns
801/// `AirMetricsDTO` with metrics data
802#[tauri::command]
803pub async fn GetAirMetrics(metric_type:Option<String>) -> Result<AirMetricsDTO, String> {
804	dev_log!("grpc", "[WindAirCommands] GetAirMetrics called with type: {:?}", metric_type);
805
806	let air_address = get_air_address()?;
807	let client = get_or_create_air_client(air_address).await?;
808
809	let request_id = uuid::Uuid::new_v4().to_string();
810
811	let metrics = client
812		.get_metrics(request_id, metric_type)
813		.await
814		.map_err(|e| format!("Failed to get Air metrics: {:?}", e))?;
815
816	let result = AirMetricsDTO {
817		memory_usage_mb:metrics.memory_usage_mb,
818		cpu_usage_percent:metrics.cpu_usage_percent,
819		average_response_time:metrics.average_response_time,
820		disk_usage_mb:metrics.disk_usage_mb,
821		network_usage_mbps:metrics.network_usage_mbps,
822	};
823
824	dev_log!("grpc", "[WindAirCommands] Air metrics retrieved");
825	Ok(result)
826}
827
828// ============================================================================
829// Helper Functions
830// ============================================================================
831
832/// Get the Air daemon address from configuration
833fn get_air_address() -> Result<String, String> {
834	// Return default Air address
835	Ok(DEFAULT_AIR_SERVER_ADDRESS.to_string())
836}
837
838/// Get or create the Air client instance
839async fn get_or_create_air_client(address:String) -> Result<AirClientModule::AirClient, String> {
840	// Create a new client each time
841	// In production, you'd use a state management pattern
842	AirClientModule::AirClient::new(&address)
843		.await
844		.map_err(|e| format!("Failed to create Air client: {:?}", e))
845}
846
847/// Register all Wind-Air commands with Tauri
848pub fn register_wind_air_commands<R:tauri::Runtime>(builder:tauri::Builder<R>) -> tauri::Builder<R> {
849	builder.invoke_handler(tauri::generate_handler![
850		CheckForUpdates,
851		DownloadUpdate,
852		ApplyUpdate,
853		DownloadFile,
854		AuthenticateUser,
855		IndexFiles,
856		SearchFiles,
857		GetAirStatus,
858		GetAirMetrics,
859	])
860}