From 9490535c29f77ba5bfd3697883cb91ceb02765cf Mon Sep 17 00:00:00 2001 From: Vincent Wirawan Date: Wed, 7 May 2025 19:48:47 -0700 Subject: [PATCH] working get config --- Cargo.lock | 5 +- interface/src-tauri/Cargo.toml | 6 +- interface/src-tauri/build.rs | 25 +- interface/src-tauri/src/grpc_client.rs | 241 ++++ interface/src-tauri/src/lib.rs | 104 +- interface/src-tauri/src/main.rs | 1337 ++---------------- interface/src-tauri/src/timestamp_serde.rs | 38 + interface/src/auto_generated_types.ts | 172 +-- interface/src/components/ScreenDashboard.tsx | 142 +- proto/lifelog_types.proto | 194 +-- 10 files changed, 739 insertions(+), 1525 deletions(-) create mode 100644 interface/src-tauri/src/grpc_client.rs create mode 100644 interface/src-tauri/src/timestamp_serde.rs diff --git a/Cargo.lock b/Cargo.lock index 56ba7fb2..236b276a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -3965,8 +3965,11 @@ dependencies = [ "dirs 5.0.1", "evdev", "hound", + "http 0.2.12", + "hyper 0.14.32", "hyprland", "image 0.24.9", + "lazy_static", "log", "prost 0.13.5", "prost-build 0.11.9", diff --git a/interface/src-tauri/Cargo.toml b/interface/src-tauri/Cargo.toml index 5bcc952d..961be38c 100644 --- a/interface/src-tauri/Cargo.toml +++ b/interface/src-tauri/Cargo.toml @@ -12,9 +12,6 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -# The `_lib` suffix may seem redundant but it is necessary -# to make the lib name unique and wouldn't conflict with the bin name. -# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 name = "lifelog_interface_lib" crate-type = ["staticlib", "cdylib", "rlib"] @@ -43,6 +40,9 @@ dirs = "5.0.1" base64 = "0.21.0" async-trait = "0.1" reqwest = { version = "0.11", features = ["json", "multipart"] } +http = "0.2" +hyper = "0.14" +lazy_static = "1.4.0" # Cross-platform dependencies toml = "0.5" diff --git a/interface/src-tauri/build.rs b/interface/src-tauri/build.rs index a663fda9..0528721d 100644 --- a/interface/src-tauri/build.rs +++ b/interface/src-tauri/build.rs @@ -3,24 +3,29 @@ use std::error::Error; use std::path::PathBuf; fn main() -> Result<(), Box> { - // Ensure this runs before the tauri_build::build() let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); tonic_build::configure() - .build_client(true) // We need the client code - .build_server(false) // We don't need server code in the interface - .file_descriptor_set_path(out_dir.join("lifelog_descriptor.bin")) // Store the descriptor set - .compile_well_known_types(true) // Generate code for well-known types so serde derive can be applied - .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") // Add serde derive attributes to generated message types + .build_client(false) + .build_server(false) + .file_descriptor_set_path(out_dir.join("lifelog_descriptor.bin")) + .compile_well_known_types(true) + .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") + .client_attribute("lifelog.LifelogServerService", "#[derive(Debug)]") + .client_attribute("lifelog.CollectorService", "#[derive(Debug)]") + .client_attribute("lifelog.LifelogServerServiceClient", "#[allow(dead_code)]") + .client_attribute("lifelog.CollectorServiceClient", "#[allow(dead_code)]") + .client_attribute(".", r#"#[allow(unused_qualifications)]"#) + .extern_path(".google.protobuf", "::prost_types") + .field_attribute("timestamp", "#[serde(with = \"crate::timestamp_serde\")]") .compile( &[ - "../../proto/lifelog.proto", // Path relative to build.rs - "../../proto/lifelog_types.proto", // Path relative to build.rs + "../../proto/lifelog.proto", + "../../proto/lifelog_types.proto", ], - &["../../proto/"], // Include path for imports within protos + &["../../proto/"], )?; - // Default Tauri build steps tauri_build::build(); Ok(()) diff --git a/interface/src-tauri/src/grpc_client.rs b/interface/src-tauri/src/grpc_client.rs new file mode 100644 index 00000000..058ebaa7 --- /dev/null +++ b/interface/src-tauri/src/grpc_client.rs @@ -0,0 +1,241 @@ +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::Mutex; +use tokio::time::timeout; +use tonic::transport::{Channel, Endpoint}; +use serde_json::Value; +use crate::lifelog; +use crate::lifelog::lifelog_server_service_client::LifelogServerServiceClient; + +pub const GRPC_SERVER_ADDRESS: &str = "http://127.0.0.1:7182"; + +const CACHE_TIMEOUT_SECS: u64 = 30; + +pub struct ConfigCache { + last_updated: Instant, + configs: std::collections::HashMap, +} + +pub struct GrpcClient { + channel: Channel, + client: LifelogServerServiceClient, + cache: Arc>>, +} + +impl GrpcClient { + pub async fn new() -> Result { + let endpoint = Endpoint::from_shared(GRPC_SERVER_ADDRESS.to_string()) + .map_err(|e| format!("Invalid endpoint URL: {}", e))?; + + let connect_result = timeout( + Duration::from_secs(2), + endpoint.connect() + ).await; + + let channel = match connect_result { + Ok(result) => result.map_err(|e| format!("Failed to connect: {}", e))?, + Err(_) => return Err("Connection timed out".to_string()), + }; + + let client = LifelogServerServiceClient::new(channel.clone()); + let cache = Arc::new(Mutex::new(None)); + + Ok(Self { + channel, + client, + cache, + }) + } + + pub async fn get_config(&self, component_name: &str) -> Result { + // Check cache first + { + let cache_guard = self.cache.lock().await; + if let Some(ref cache_data) = *cache_guard { + // Cache valid for 30 seconds + if cache_data.last_updated.elapsed() < Duration::from_secs(CACHE_TIMEOUT_SECS) { + if let Some(config) = cache_data.configs.get(component_name) { + println!("Using cached config for {}", component_name); + return Ok(config.clone()); + } + } + } + } + + // No valid cache, get from server + let request = tonic::Request::new(lifelog::GetSystemConfigRequest {}); + + let response_result = timeout( + Duration::from_secs(5), + self.client.clone().get_config(request) + ).await; + + let response = match response_result { + Ok(result) => result.map_err(|e| format!("gRPC GetConfig error: {}", e))?.into_inner(), + Err(_) => return Err("gRPC call timed out".to_string()), + }; + + let collector_config = response.config.ok_or_else(|| "No config returned from server".to_string())? + .collector.ok_or_else(|| "No collector config returned from server".to_string())?; + + let mut new_cache = std::collections::HashMap::new(); + + if let Some(screen_config) = &collector_config.screen { + if let Ok(value) = serde_json::to_value(screen_config) { + new_cache.insert("screen".to_string(), value.clone()); + if component_name == "screen" { + self.update_cache(new_cache).await; + return Ok(value); + } + } + } + + if let Some(camera_config) = &collector_config.camera { + if let Ok(value) = serde_json::to_value(camera_config) { + new_cache.insert("camera".to_string(), value.clone()); + if component_name == "camera" { + self.update_cache(new_cache).await; + return Ok(value); + } + } + } + + if let Some(microphone_config) = &collector_config.microphone { + if let Ok(value) = serde_json::to_value(microphone_config) { + new_cache.insert("microphone".to_string(), value.clone()); + if component_name == "microphone" { + self.update_cache(new_cache).await; + return Ok(value); + } + } + } + + if let Some(processes_config) = &collector_config.processes { + if let Ok(value) = serde_json::to_value(processes_config) { + new_cache.insert("processes".to_string(), value.clone()); + if component_name == "processes" { + self.update_cache(new_cache).await; + return Ok(value); + } + } + } + + // Update cache even if requested component wasn't found + self.update_cache(new_cache).await; + + Err(format!("Component {} not found in server response", component_name)) + } + + pub async fn set_config(&self, component_name: &str, config_value: &Value) -> Result<(), String> { + // First get current config + let request = tonic::Request::new(lifelog::GetSystemConfigRequest {}); + + let response_result = timeout( + Duration::from_secs(5), + self.client.clone().get_config(request) + ).await; + + let get_response = match response_result { + Ok(result) => result.map_err(|e| format!("gRPC GetConfig error: {}", e))?.into_inner(), + Err(_) => return Err("gRPC call timed out".to_string()), + }; + + let mut system_config = get_response.config + .ok_or_else(|| "No config returned from server".to_string())?; + + if system_config.collector.is_none() { + return Err("No collector config in system config".to_string()); + } + + let mut collector_config = system_config.collector.unwrap(); + + match component_name.to_lowercase().as_str() { + "screen" => { + let screen_config: lifelog::ScreenConfig = serde_json::from_value(config_value.clone()) + .map_err(|e| format!("Failed to deserialize screen config: {}", e))?; + collector_config.screen = Some(screen_config); + }, + "camera" => { + let camera_config: lifelog::CameraConfig = serde_json::from_value(config_value.clone()) + .map_err(|e| format!("Failed to deserialize camera config: {}", e))?; + collector_config.camera = Some(camera_config); + }, + "microphone" => { + let microphone_config: lifelog::MicrophoneConfig = serde_json::from_value(config_value.clone()) + .map_err(|e| format!("Failed to deserialize microphone config: {}", e))?; + collector_config.microphone = Some(microphone_config); + }, + "processes" => { + let processes_config: lifelog::ProcessesConfig = serde_json::from_value(config_value.clone()) + .map_err(|e| format!("Failed to deserialize processes config: {}", e))?; + collector_config.processes = Some(processes_config); + }, + _ => return Err(format!("Unsupported component: {}", component_name)), + }; + + system_config.collector = Some(collector_config); + + let set_request = tonic::Request::new(lifelog::SetSystemConfigRequest { + config: Some(system_config), + }); + + let set_response_result = timeout( + Duration::from_secs(5), + self.client.clone().set_config(set_request) + ).await; + + let set_response = match set_response_result { + Ok(result) => result.map_err(|e| format!("gRPC SetConfig error: {}", e))?.into_inner(), + Err(_) => return Err("gRPC set_config call timed out".to_string()), + }; + + if !set_response.success { + return Err("Server reported failure in setting config".to_string()); + } + + self.invalidate_cache().await; + + Ok(()) + } + + async fn update_cache(&self, new_cache: std::collections::HashMap) { + let mut cache_guard = self.cache.lock().await; + *cache_guard = Some(ConfigCache { + last_updated: Instant::now(), + configs: new_cache, + }); + } + + pub async fn invalidate_cache(&self) { + let mut cache_guard = self.cache.lock().await; + *cache_guard = None; + } +} + +lazy_static::lazy_static! { + pub static ref GRPC_CLIENT: tokio::sync::Mutex>> = tokio::sync::Mutex::new(None); +} + +pub async fn init_grpc_client() -> Result<(), String> { + let mut client_guard = GRPC_CLIENT.lock().await; + if client_guard.is_none() { + match GrpcClient::new().await { + Ok(client) => { + *client_guard = Some(Arc::new(client)); + Ok(()) + }, + Err(e) => Err(format!("Failed to initialize gRPC client: {}", e)), + } + } else { + Ok(()) + } +} + +pub async fn get_grpc_client() -> Result, String> { + let client_guard = GRPC_CLIENT.lock().await; + if let Some(client) = &*client_guard { + Ok(client.clone()) + } else { + Err("gRPC client not initialized".to_string()) + } +} \ No newline at end of file diff --git a/interface/src-tauri/src/lib.rs b/interface/src-tauri/src/lib.rs index ad783d1b..ff159a79 100644 --- a/interface/src-tauri/src/lib.rs +++ b/interface/src-tauri/src/lib.rs @@ -1,22 +1,11 @@ -// Export all modules needed by the main binary -// pub mod config; - Using common config crate instead pub mod embed { // Empty placeholder module } -// No longer needed with the new API-based architecture -// pub mod modules { -// // Empty placeholder module -// } - pub mod prelude; pub mod setup; pub mod storage; -// pub mod utils; - Using common utils crate instead - -// Re-export commonly used items pub use config::*; -// pub use modules::*; // No longer using direct module access pub use setup::*; pub use utils::*; @@ -56,21 +45,107 @@ pub mod config_utils { let config = load_config(); config.microphone.clone() } + + // Add saving functions + pub fn save_screen_config(screen_config: &config::ScreenConfig) { + match dirs::home_dir() { + Some(home_dir) => { + let config_dir = home_dir.join(".lifelog"); + std::fs::create_dir_all(&config_dir).unwrap_or_else(|_| { + println!("Could not create config directory"); + }); + let config_file = config_dir.join("screen_config.json"); + let config_json = serde_json::to_string_pretty(screen_config).unwrap_or_else(|_| { + println!("Could not serialize screen config"); + "{}".to_string() + }); + std::fs::write(config_file, config_json).unwrap_or_else(|_| { + println!("Could not write screen config file"); + }); + } + None => { + println!("Could not get home directory"); + } + } + } + + pub fn save_microphone_config(microphone_config: &config::MicrophoneConfig) { + match dirs::home_dir() { + Some(home_dir) => { + let config_dir = home_dir.join(".lifelog"); + std::fs::create_dir_all(&config_dir).unwrap_or_else(|_| { + println!("Could not create config directory"); + }); + let config_file = config_dir.join("microphone_config.json"); + let config_json = serde_json::to_string_pretty(microphone_config).unwrap_or_else(|_| { + println!("Could not serialize microphone config"); + "{}".to_string() + }); + std::fs::write(config_file, config_json).unwrap_or_else(|_| { + println!("Could not write microphone config file"); + }); + } + None => { + println!("Could not get home directory"); + } + } + } + + pub fn save_text_upload_config(text_config: &config::TextUploadConfig) { + match dirs::home_dir() { + Some(home_dir) => { + let config_dir = home_dir.join(".lifelog"); + std::fs::create_dir_all(&config_dir).unwrap_or_else(|_| { + println!("Could not create config directory"); + }); + let config_file = config_dir.join("text_upload_config.json"); + let config_json = serde_json::to_string_pretty(text_config).unwrap_or_else(|_| { + println!("Could not serialize text upload config"); + "{}".to_string() + }); + std::fs::write(config_file, config_json).unwrap_or_else(|_| { + println!("Could not write text upload config file"); + }); + } + None => { + println!("Could not get home directory"); + } + } + } + + pub fn save_processes_config(processes_config: &config::ProcessesConfig) { + match dirs::home_dir() { + Some(home_dir) => { + let config_dir = home_dir.join(".lifelog"); + std::fs::create_dir_all(&config_dir).unwrap_or_else(|_| { + println!("Could not create config directory"); + }); + let config_file = config_dir.join("processes_config.json"); + let config_json = serde_json::to_string_pretty(processes_config).unwrap_or_else(|_| { + println!("Could not serialize processes config"); + "{}".to_string() + }); + std::fs::write(config_file, config_json).unwrap_or_else(|_| { + println!("Could not write processes config file"); + }); + } + None => { + println!("Could not get home directory"); + } + } + } } -// New API client module for communicating with the server pub mod api_client { use reqwest::Client; use serde::{Deserialize, Serialize}; use std::env; use std::time::Duration; - // API base URL - defaults to localhost:8080 if not set pub fn get_api_base_url() -> String { env::var("VITE_API_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()) } - // Create a new API client with reasonable defaults pub fn create_client() -> Client { Client::builder() .timeout(Duration::from_secs(30)) @@ -78,7 +153,6 @@ pub mod api_client { .expect("Failed to create HTTP client") } - // Generic API response structure #[derive(Debug, Serialize, Deserialize)] pub struct ApiResponse { pub success: bool, diff --git a/interface/src-tauri/src/main.rs b/interface/src-tauri/src/main.rs index 401dfb43..28cb1924 100644 --- a/interface/src-tauri/src/main.rs +++ b/interface/src-tauri/src/main.rs @@ -1,1244 +1,164 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] -mod storage; - -use crate::storage::AudioFile; -use base64; -use base64::{engine::general_purpose, Engine as _}; -use chrono::Local; -use dirs; - -use config::{MicrophoneConfig, ProcessesConfig, ScreenConfig, TextUploadConfig}; -use lifelog_interface_lib::{ - api_client, - config_utils, -}; +use config::{ScreenConfig, MicrophoneConfig, ProcessesConfig, TextUploadConfig}; +use lifelog_interface_lib::config_utils; use serde::{Deserialize, Serialize}; -use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::Command; use std::sync::Mutex; use tauri::State; use serde_json::Value; -use tonic::transport::Channel; - -// Include generated code for well-known google protobuf types -pub mod google { - pub mod protobuf { - tonic::include_proto!("google.protobuf"); - } -} -// This module contains the Protobuf generated code -pub mod lifelog { - tonic::include_proto!("lifelog"); -} - -// Define application state -struct AppState { +// AppState to store configurations +pub struct AppState { text_config: Mutex, processes_config: Mutex, screen_config: Mutex, - api_client: reqwest::Client, -} - -// Define types for frontend -#[derive(Serialize, Deserialize, Clone)] -struct TextFile { - filename: String, - original_path: String, - file_type: String, - file_size: u64, - stored_path: String, - content_hash: String, -} - -#[derive(Serialize, Deserialize, Clone)] -struct Process { - pid: i32, - parent_pid: i32, - name: String, - executable: Option, - command: String, - status: String, - cpu_usage: f32, - memory: i64, - runtime: i32, - user: Option, - start_time: f64, -} - -#[derive(Serialize, Deserialize, Clone)] -struct Screenshot { - id: i32, - timestamp: f64, - path: String, -} - -// Text upload commands -#[tauri::command] -async fn get_all_text_files(state: State<'_, AppState>) -> Result, String> { - let url = format!("{}/api/loggers/text/data", api_client::get_api_base_url()); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - let files = data - .data - .unwrap_or_default() - .into_iter() - .filter_map(|item| { - Some(TextFile { - filename: item.get("filename")?.as_str()?.to_string(), - original_path: item.get("original_path")?.as_str()?.to_string(), - file_type: item.get("file_type")?.as_str()?.to_string(), - file_size: item.get("file_size")?.as_u64()?, - stored_path: item.get("stored_path")?.as_str()?.to_string(), - content_hash: item.get("content_hash")?.as_str()?.to_string(), - }) - }) - .collect(); - - Ok(files) -} - -#[tauri::command] -async fn search_text_files( - pattern: String, - state: State<'_, AppState>, -) -> Result, String> { - let url = format!( - "{}/api/loggers/text/search?pattern={}", - api_client::get_api_base_url(), - pattern - ); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - let files = data - .data - .unwrap_or_default() - .into_iter() - .filter_map(|item| { - Some(TextFile { - filename: item.get("filename")?.as_str()?.to_string(), - original_path: item.get("original_path")?.as_str()?.to_string(), - file_type: item.get("file_type")?.as_str()?.to_string(), - file_size: item.get("file_size")?.as_u64()?, - stored_path: item.get("stored_path")?.as_str()?.to_string(), - content_hash: item.get("content_hash")?.as_str()?.to_string(), - }) - }) - .collect(); - - Ok(files) -} - -// This needs to be fixed for Send/Sync issues with MutexGuard -#[tauri::command] -async fn upload_text_file( - file_path: String, - state: State<'_, AppState>, -) -> Result { - let path = PathBuf::from(&file_path); - if !path.exists() { - return Err(format!("File not found: {}", file_path)); - } - - if let Err(e) = std::fs::File::open(&path) { - return Err(format!("Cannot read file: {}", e)); - } - - let file_content = match std::fs::read(&path) { - Ok(content) => content, - Err(e) => return Err(format!("Failed to read file: {}", e)), - }; - - let form = reqwest::multipart::Form::new() - .text("file_path", file_path.clone()) - .part( - "file", - reqwest::multipart::Part::bytes(file_content) - .file_name(path.file_name().unwrap_or_default().to_string_lossy().into_owned()), - ); - - // Use API client to upload file - let url = format!("{}/api/loggers/text/upload", api_client::get_api_base_url()); - - let response = state - .api_client - .post(&url) - .multipart(form) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - let file_data = data.data.ok_or("No file data returned from server")?; - - Ok(TextFile { - filename: file_data.get("filename").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - original_path: file_data.get("original_path").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - file_type: file_data.get("file_type").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - file_size: file_data.get("file_size").and_then(|v| v.as_u64()).unwrap_or_default(), - stored_path: file_data.get("stored_path").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - content_hash: file_data.get("content_hash").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - }) -} - -#[tauri::command] -async fn select_file_dialog() -> Result { - //Temporary fixed path for testing - #[cfg(target_os = "macos")] - let path = "/Users/vincenw/Documents/test.txt".to_string(); - - #[cfg(target_os = "windows")] - let path = "C:\\Users\\Documents\\test.txt".to_string(); - - #[cfg(target_os = "linux")] - let path = "/home/user/Documents/test.txt".to_string(); - - Ok(path) } -// Process commands -#[tauri::command] -async fn get_current_processes(state: State<'_, AppState>) -> Result, String> { - let url = format!("{}/api/loggers/processes/current", api_client::get_api_base_url()); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - Ok(data.data.unwrap_or_default()) -} - -#[tauri::command] -async fn get_process_history( - start_time: Option, - end_time: Option, - limit: Option, - process_name: Option, - state: State<'_, AppState>, -) -> Result, String> { - let mut url = format!("{}/api/loggers/processes/data?", api_client::get_api_base_url()); +// Helper function to get component config via gRPC +async fn get_grpc_component_config(component_name: &str) -> Result { + println!("Attempting to get config for {} via gRPC", component_name); - // Add query parameters - if let Some(start) = start_time { - url.push_str(&format!("start_time={}&", start)); - } + let output = Command::new("grpcurl") + .args([ + "-plaintext", + "-d", "{}", + "127.0.0.1:7182", + "lifelog.LifelogServerService/GetConfig" + ]) + .output() + .map_err(|e| format!("Failed to execute gRPC client: {}", e))?; - if let Some(end) = end_time { - url.push_str(&format!("end_time={}&", end)); + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("gRPC client error: {}", stderr); + return Err(format!("gRPC client error: {}", stderr)); } - if let Some(limit_val) = limit { - url.push_str(&format!("limit={}&", limit_val)); - } + let stdout = String::from_utf8_lossy(&output.stdout); + println!("gRPC raw response: {}", stdout); - if let Some(name) = process_name { - url.push_str(&format!("process_name={}&", name)); - } + let response: serde_json::Value = serde_json::from_str(&stdout) + .map_err(|e| format!("Failed to parse gRPC response: {}", e))?; - // Remove trailing & - if url.ends_with('&') { - url.pop(); - } - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - Ok(data.data.unwrap_or_default()) -} - -// Screenshot commands -#[tauri::command] -async fn get_screenshots( - page: u32, - page_size: u32, - state: State<'_, AppState>, -) -> Result, String> { - let url = format!( - "{}/api/loggers/screen/data?page={}&pageSize={}", - api_client::get_api_base_url(), - page, - page_size - ); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - let screenshots = data - .data - .unwrap_or_default() - .into_iter() - .filter_map(|item| { - let id = item.get("id")?.as_i64()? as i32; - let timestamp = item.get("timestamp")?.as_f64()?; - let path = item.get("path")?.as_str()?.to_string(); - - Some(Screenshot { - id, - timestamp, - path, - }) - }) - .collect(); - - Ok(screenshots) -} - -#[tauri::command] -async fn get_screenshot_settings(state: State<'_, AppState>) -> Result { - let url = format!( - "{}/api/loggers/screen/config", - api_client::get_api_base_url() - ); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - // Use the data if available, otherwise create a minimal config - // This replaces the Default trait implementation - match data.data { - Some(config) => Ok(config), - None => { - // Get config from local - let config = config_utils::load_screen_config(); - Ok(config) - } - } -} - -#[tauri::command] -async fn update_screenshot_settings( - enabled: bool, - interval: f64, - state: State<'_, AppState>, -) -> Result<(), String> { - let url = format!( - "{}/api/loggers/screen/config", - api_client::get_api_base_url() - ); - - let payload = serde_json::json!({ - "enabled": enabled, - "interval": interval - }); - - let response = state - .api_client - .put(&url) - .json(&payload) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - // Update local config - let mut config = state.screen_config.lock().unwrap(); - config.enabled = enabled; - config.interval = interval; - - Ok(()) -} - -#[tauri::command] -async fn get_screenshot_data(filename: String, state: State<'_, AppState>) -> Result { - let url = format!( - "{}/api/files/screen/{}", - api_client::get_api_base_url(), - filename - ); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - // Get the bytes and convert to base64 - let bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to get response bytes: {}", e))?; - - let base64_data = general_purpose::STANDARD.encode(&bytes); - Ok(base64_data) -} - -#[tauri::command] -async fn stop_screen_capture(state: State<'_, AppState>) -> Result<(), String> { - let response = state - .api_client - .post(&format!("{}/api/loggers/screen/stop", api_client::get_api_base_url())) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if response.status().is_success() { - Ok(()) - } else { - Err(format!( - "Server returned error: {}", - response.status().as_u16() - )) - } -} - -#[tauri::command] -async fn start_screen_capture( - interval: Option, - state: State<'_, AppState>, -) -> Result<(), String> { - let mut payload = serde_json::json!({}); + println!("gRPC response parsed: {:?}", response); - if let Some(interval_val) = interval { - payload = serde_json::json!({ - "interval": interval_val - }); - } - - let response = state - .api_client - .post(&format!("{}/api/loggers/screen/start", api_client::get_api_base_url())) - .json(&payload) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if response.status().is_success() { - Ok(()) - } else { - Err(format!( - "Server returned error: {}", - response.status().as_u16() - )) - } -} - -#[tauri::command] -async fn initialize_app( - _window: tauri::Window, - _app_handle: tauri::AppHandle, - _state: State<'_, AppState>, -) -> Result<(), String> { - Ok(()) -} - -// Add Camera APIs -#[tauri::command] -async fn is_camera_supported() -> bool { - #[cfg(target_os = "linux")] - return true; - - #[cfg(target_os = "macos")] - { - // First check if imagesnap is installed - let imagesnap_installed = match Command::new("which").arg("imagesnap").output() { - Ok(output) => output.status.success(), - Err(_) => false, - }; - - if !imagesnap_installed { - println!("Camera check: imagesnap utility not found, camera will not work"); - return false; - } - - // Now check if any cameras are detected - match Command::new("imagesnap").arg("-l").output() { - Ok(output) => { - let output_str = String::from_utf8_lossy(&output.stdout); - - // Check if any devices are listed - if output_str.contains("Video Devices:") - && (output_str.contains("Camera") - || output_str.contains("FaceTime") - || output_str.contains("iPhone") - || output_str.contains("Webcam")) - { - println!("Camera check: Detected cameras: {}", output_str.trim()); - - // Try to check for camera permissions - let temp_path = std::env::temp_dir() - .join(format!("lifelog_cam_test_{}.jpg", std::process::id())); - match Command::new("imagesnap") - .arg(temp_path.to_str().unwrap_or("/tmp/test.jpg")) - .arg("-w") - .arg("0.1") // Short warm-up to not block - .output() - { - Ok(capture) => { - let success = capture.status.success(); - // Clean up test file - let _ = std::fs::remove_file(temp_path); - - if !success { - let stderr = String::from_utf8_lossy(&capture.stderr); - println!("Camera check: Permission issue detected: {}", stderr); - return false; - } - - println!("Camera check: Successfully captured test image"); - return true; - } - Err(e) => { - println!("Camera check: Failed to run test capture: {}", e); - return false; - } - } - } else { - println!("Camera check: No cameras detected"); - return false; - } - } - Err(e) => { - println!("Camera check: Failed to list cameras: {}", e); - return false; - } - } - } - - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - { - println!("Camera check: Platform not supported"); - return false; - } -} - -#[tauri::command] -async fn get_camera_settings( - config_manager: tauri::State<'_, Mutex>, -) -> Result { - // Get the camera_config and release the mutex guard immediately - let camera_config = { - let config_manager = config_manager.lock().map_err(|e| e.to_string())?; - config_manager.get_camera_config() + // Extract the specific component config from the full response + let result = match component_name.to_lowercase().as_str() { + "screen" => { + response.get("config") + .and_then(|c| c.get("collector")) + .and_then(|c| c.get("screen")) + .cloned() + .ok_or_else(|| "No screen config found in response".to_string()) + }, + "camera" => { + response.get("config") + .and_then(|c| c.get("collector")) + .and_then(|c| c.get("camera")) + .cloned() + .ok_or_else(|| "No camera config found in response".to_string()) + }, + "microphone" => { + response.get("config") + .and_then(|c| c.get("collector")) + .and_then(|c| c.get("microphone")) + .cloned() + .ok_or_else(|| "No microphone config found in response".to_string()) + }, + "processes" => { + response.get("config") + .and_then(|c| c.get("collector")) + .and_then(|c| c.get("processes")) + .cloned() + .ok_or_else(|| "No processes config found in response".to_string()) + }, + _ => Err(format!("Unsupported component: {}", component_name)), }; - - Ok(serde_json::json!({ - "enabled": camera_config.enabled, - "device": camera_config.device, - "fps": camera_config.fps, - "interval": camera_config.interval, - "output_dir": camera_config.output_dir, - "resolution": camera_config.resolution, - "timestamp_format": camera_config.timestamp_format, - })) -} - -#[tauri::command] -async fn update_camera_settings( - enabled: bool, - interval: f64, - fps: u32, - config_manager: tauri::State<'_, Mutex>, - state: State<'_, AppState>, -) -> Result<(), String> { - let url = format!("{}/api/loggers/camera/config", api_client::get_api_base_url()); - let payload = serde_json::json!({ - "enabled": enabled, - "interval": interval, - "fps": fps - }); - - let response = state - .api_client - .put(&url) - .json(&payload) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - // Update local settings - { - let mut config_manager = config_manager.lock().map_err(|e| e.to_string())?; - let mut camera_config = config_manager.get_camera_config(); - - camera_config.enabled = enabled; - camera_config.interval = interval; - camera_config.fps = fps; - - config_manager.set_camera_config(camera_config); - config_manager.save().map_err(|e| e.to_string())?; + // Log the result + match &result { + Ok(value) => println!("Successfully extracted config for {}: {:?}", component_name, value), + Err(e) => println!("Failed to extract config for {}: {}", component_name, e), } - - Ok(()) -} - -#[tauri::command] -async fn get_camera_frames( - page: usize, - page_size: usize, - config_manager: tauri::State<'_, Mutex>, - state: State<'_, AppState>, -) -> Result, String> { - let url = format!( - "{}/api/loggers/camera/data?page={}&pageSize={}", - api_client::get_api_base_url(), - page, - page_size - ); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - Ok(data.data.unwrap_or_default()) -} - -#[tauri::command] -async fn get_camera_frame_data(filename: String, state: State<'_, AppState>) -> Result { - let url = format!( - "{}/api/files/camera/{}", - api_client::get_api_base_url(), - filename - ); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - // Get the bytes and convert to base64 - let bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to get response bytes: {}", e))?; - - let base64_data = general_purpose::STANDARD.encode(&bytes); - let mime_type = "image/jpeg"; // Assuming all frames are jpg - Ok(format!("data:{};base64,{}", mime_type, base64_data)) + result } -#[tauri::command] -async fn trigger_camera_capture( - config_manager: tauri::State<'_, Mutex>, - state: State<'_, AppState>, -) -> Result<(), String> { - let url = format!("{}/api/loggers/camera/capture", api_client::get_api_base_url()); +// Helper function to get local component config +fn get_local_component_config(component_name: &str) -> Result { + println!("Using local config for component: {}", component_name); - let response = state - .api_client - .post(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); + match component_name.to_lowercase().as_str() { + "screen" => { + let screen_config = lifelog_interface_lib::config_utils::load_screen_config(); + serde_json::to_value(screen_config) + .map_err(|e| format!("Failed to serialize local screen config: {}", e)) + }, + "camera" => { + let camera_config = config::CameraConfig { + enabled: false, + interval: 60.0, + output_dir: std::path::PathBuf::from("~/lifelog/camera"), + device: String::new(), + fps: 30, + resolution_x: 1280, + resolution_y: 720, + timestamp_format: "%Y-%m-%d_%H-%M-%S".to_string(), + }; + serde_json::to_value(camera_config) + .map_err(|e| format!("Failed to serialize local camera config: {}", e)) + }, + "microphone" => { + let microphone_config = lifelog_interface_lib::config_utils::load_microphone_config(); + serde_json::to_value(microphone_config) + .map_err(|e| format!("Failed to serialize local microphone config: {}", e)) + }, + "processes" => { + let processes_config = lifelog_interface_lib::config_utils::load_processes_config(); + serde_json::to_value(processes_config) + .map_err(|e| format!("Failed to serialize local processes config: {}", e)) + }, + _ => Err(format!("Unsupported component: {}", component_name)), } - - Ok(()) } +// Command to get component configuration #[tauri::command] -async fn restart_camera_logger( - config_manager: tauri::State<'_, Mutex>, - state: State<'_, AppState>, -) -> Result<(), String> { - let url = format!("{}/api/loggers/camera/restart", api_client::get_api_base_url()); +async fn get_component_config(component_name: String) -> Result { + println!("Requesting config for component: {}", component_name); - let response = state - .api_client - .post(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - // Capture one frame immediately to verify it works - println!("Capturing initial test frame"); - let test_result = trigger_camera_capture(config_manager.clone(), state.clone()).await; - if let Err(e) = &test_result { - println!("Warning: Initial test capture failed: {}", e); - // Continue anyway, maybe it will work with the logger - } else { - println!("Initial test capture successful"); - } - - Ok(()) -} - -// Microphone commands -#[tauri::command] -async fn get_microphone_settings(state: State<'_, AppState>) -> Result { - let url = format!("{}/api/loggers/microphone/config", api_client::get_api_base_url()); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - // Use the data if available, otherwise create a minimal config - // This replaces the Default trait implementation - match data.data { - Some(config) => Ok(config), - None => { - // Get config from local - let config = config_utils::load_microphone_config(); + match get_grpc_component_config(&component_name).await { + Ok(config) => { + println!("Retrieved config for {} from gRPC server", component_name); Ok(config) + }, + Err(e) => { + println!("Failed to get config from gRPC, using local fallback: {}", e); + get_local_component_config(&component_name) } } } +// Initialize app command - minimal implementation #[tauri::command] -async fn start_microphone_recording(state: State<'_, AppState>) -> Result<(), String> { - let url = format!("{}/api/loggers/microphone/record/start", api_client::get_api_base_url()); - - let response = state - .api_client - .post(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - Ok(()) -} - -#[tauri::command] -async fn stop_microphone_recording(state: State<'_, AppState>) -> Result<(), String> { - let url = format!("{}/api/loggers/microphone/record/stop", api_client::get_api_base_url()); - - let response = state - .api_client - .post(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - Ok(()) -} - -#[tauri::command] -async fn pause_microphone_recording(state: State<'_, AppState>) -> Result<(), String> { - let url = format!("{}/api/loggers/microphone/record/pause", api_client::get_api_base_url()); - - let response = state - .api_client - .post(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - Ok(()) -} - -#[tauri::command] -async fn resume_microphone_recording(state: State<'_, AppState>) -> Result<(), String> { - let url = format!("{}/api/loggers/microphone/record/resume", api_client::get_api_base_url()); - - let response = state - .api_client - .post(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - Ok(()) -} - -#[tauri::command] -async fn get_audio_files( - page: usize, - page_size: usize, - state: State<'_, AppState> -) -> Result, String> { - // Use API client to get audio files - let url = format!( - "{}/api/loggers/microphone/data?page={}&pageSize={}", - api_client::get_api_base_url(), - page, - page_size - ); - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - Ok(data.data.unwrap_or_default()) -} - -// Load processes from database with pagination -#[tauri::command] -async fn get_all_processes( - start_time: Option, - end_time: Option, - limit: Option, - process_name: Option, - state: State<'_, AppState>, -) -> Result, String> { - let mut url = format!("{}/api/loggers/processes/history?", api_client::get_api_base_url()); - - // Add query parameters - if let Some(start) = start_time { - url.push_str(&format!("start_time={}&", start)); - } - - if let Some(end) = end_time { - url.push_str(&format!("end_time={}&", end)); - } - - if let Some(limit_val) = limit { - url.push_str(&format!("limit={}&", limit_val)); - } - - if let Some(name) = process_name { - url.push_str(&format!("process_name={}&", name)); - } - - // Remove trailing & - if url.ends_with('&') { - url.pop(); - } - - let response = state - .api_client - .get(&url) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - let data = response - .json::>>() - .await - .map_err(|e| format!("Failed to parse response: {}", e))?; - - if !data.success { - return Err(data.error.unwrap_or_else(|| "Unknown error".to_string())); - } - - Ok(data.data.unwrap_or_default()) -} - -#[tauri::command] -async fn update_microphone_settings( - enabled: bool, - chunk_duration_secs: u64, - capture_interval_secs: Option, - state: State<'_, AppState>, -) -> Result<(), String> { - let url = format!("{}/api/loggers/microphone/config", api_client::get_api_base_url()); - - let mut payload = serde_json::json!({ - "enabled": enabled, - "chunk_duration_secs": chunk_duration_secs - }); - - if let Some(interval) = capture_interval_secs { - payload["capture_interval_secs"] = serde_json::json!(interval); - } - - let response = state - .api_client - .put(&url) - .json(&payload) - .send() - .await - .map_err(|e| format!("Failed to send request: {}", e))?; - - if !response.status().is_success() { - return Err(format!( - "Server returned error: {}", - response.status().as_u16() - )); - } - - Ok(()) -} - -// Server address remains the same for now -const GRPC_SERVER_ADDRESS: &str = "http://127.0.0.1:7182"; - -#[tauri::command] -async fn get_component_config(component_name: String) -> Result { - println!("Attempting to get config for component: {} via ServerService", component_name); - println!("gRPC: get_component_config - connecting to {}", GRPC_SERVER_ADDRESS); - let channel = Channel::from_static(GRPC_SERVER_ADDRESS) - .connect() - .await - .map_err(|e| format!("Failed to connect to gRPC server: {}", e))?; - println!("gRPC: get_component_config - connection established"); - let mut client = lifelog::lifelog_server_service_client::LifelogServerServiceClient::new(channel); - let request = tonic::Request::new(lifelog::GetSystemConfigRequest {}); - let response = client - .get_config(request) - .await - .map_err(|e| format!("ServerService GetConfig gRPC request failed: {}", e))?; - println!("gRPC: get_component_config - RPC get_config succeeded: {:?}", response); - let system_config = response.into_inner().config.ok_or_else(|| { - "Server response did not contain SystemConfig data".to_string() - })?; - // Parse collectors JSON string into CollectorConfig - let collectors_json = system_config.collectors; - println!("gRPC: get_component_config - received collectors JSON: {}", collectors_json); - let collector_config: lifelog::CollectorConfig = serde_json::from_str(&collectors_json) - .map_err(|e| format!("Failed to parse collector config JSON: {}", e))?; - println!("gRPC: get_component_config - parsed CollectorConfig: {:?}", collector_config); - let component_value = match component_name.to_lowercase().as_str() { - "screen" => serde_json::to_value(&collector_config.screen) - .map_err(|e| format!("Failed to serialize screen config: {}", e))?, - "camera" => serde_json::to_value(&collector_config.camera) - .map_err(|e| format!("Failed to serialize camera config: {}", e))?, - "microphone" => serde_json::to_value(&collector_config.microphone) - .map_err(|e| format!("Failed to serialize microphone config: {}", e))?, - "processes" => serde_json::to_value(&collector_config.processes) - .map_err(|e| format!("Failed to serialize processes config: {}", e))?, - "hyprland" => serde_json::to_value(&collector_config.hyprland) - .map_err(|e| format!("Failed to serialize hyprland config: {}", e))?, - _ => return Err(format!("Unknown component name: {}", component_name)), - }; - println!("gRPC: get_component_config - returning component '{}' value: {:?}", component_name, component_value); - Ok(component_value) -} - -#[tauri::command] -async fn set_component_config(component_name: String, config_value: Value) -> Result<(), String> { - println!("gRPC: set_component_config - connecting to {}", GRPC_SERVER_ADDRESS); - println!("Attempting to set config for component: {} via ServerService with data: {:?}", component_name, config_value); - let channel = Channel::from_static(GRPC_SERVER_ADDRESS) - .connect() - .await - .map_err(|e| format!("Failed to connect to gRPC server: {}", e))?; - println!("gRPC: set_component_config - connection established"); - let mut client = lifelog::lifelog_server_service_client::LifelogServerServiceClient::new(channel); - let get_request = tonic::Request::new(lifelog::GetSystemConfigRequest {}); - let get_response = client - .get_config(get_request) - .await - .map_err(|e| format!("Failed to get current SystemConfig: {}", e))?; - println!("gRPC: set_component_config - RPC get_config succeeded: {:?}", get_response); - let system_config = get_response.into_inner().config.ok_or_else(|| { - "Server response did not contain SystemConfig data".to_string() - })?; - let collectors_json = system_config.collectors; - let mut collector_config: lifelog::CollectorConfig = serde_json::from_str(&collectors_json) - .map_err(|e| format!("Failed to parse collector config JSON: {}", e))?; - println!("gRPC: set_component_config - parsed CollectorConfig: {:?}", collector_config); - match component_name.to_lowercase().as_str() { - "screen" => { - let new_conf: lifelog::ScreenConfig = serde_json::from_value(config_value) - .map_err(|e| format!("Invalid screen config format: {}", e))?; - collector_config.screen = Some(new_conf); - } - "camera" => { - let new_conf: lifelog::CameraConfig = serde_json::from_value(config_value) - .map_err(|e| format!("Invalid camera config format: {}", e))?; - collector_config.camera = Some(new_conf); - } - "microphone" => { - let new_conf: lifelog::MicrophoneConfig = serde_json::from_value(config_value) - .map_err(|e| format!("Invalid microphone config format: {}", e))?; - collector_config.microphone = Some(new_conf); - } - "processes" => { - let new_conf: lifelog::ProcessesConfig = serde_json::from_value(config_value) - .map_err(|e| format!("Invalid processes config format: {}", e))?; - collector_config.processes = Some(new_conf); - } - "hyprland" => { - let new_conf: lifelog::HyprlandConfig = serde_json::from_value(config_value) - .map_err(|e| format!("Invalid hyprland config format: {}", e))?; - collector_config.hyprland = Some(new_conf); - } - _ => return Err(format!("Unknown component name for setting config: {}", component_name)), - } - println!("gRPC: set_component_config - sending updated CollectorConfig: {:?}", collector_config); - let set_request = tonic::Request::new(lifelog::SetSystemConfigRequest { - config: Some(collector_config.clone()), - }); - let set_response = client - .set_config(set_request) - .await - .map_err(|e| format!("ServerService SetConfig gRPC request failed: {}", e))?; - println!("gRPC: set_component_config - RPC set_config succeeded: {:?}", set_response); - let success_flag = set_response.into_inner().success; - if !success_flag { - return Err("Server failed to apply the new configuration.".to_string()); - } +async fn initialize_app() -> Result<(), String> { + println!("App initialized"); Ok(()) } fn main() { let config_manager = Mutex::new(config_utils::ConfigManager::new()); - // Create the application state with API client + // Create the application state let app_state = AppState { text_config: Mutex::new(lifelog_interface_lib::config_utils::load_text_upload_config()), processes_config: Mutex::new(lifelog_interface_lib::config_utils::load_processes_config()), screen_config: Mutex::new(lifelog_interface_lib::config_utils::load_screen_config()), - api_client: api_client::create_client(), }; tauri::Builder::default() @@ -1247,43 +167,10 @@ fn main() { .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ - // Text upload - get_all_text_files, - search_text_files, - upload_text_file, - select_file_dialog, - // Processes - get_current_processes, - get_process_history, - // Screenshots - get_screenshots, - get_screenshot_settings, - update_screenshot_settings, - get_screenshot_data, - stop_screen_capture, - start_screen_capture, - // Camera - is_camera_supported, - get_camera_settings, - update_camera_settings, - get_camera_frames, - get_camera_frame_data, - trigger_camera_capture, - restart_camera_logger, - // Microphone - get_microphone_settings, - update_microphone_settings, - start_microphone_recording, - stop_microphone_recording, - pause_microphone_recording, - resume_microphone_recording, - get_audio_files, - // General - get_all_processes, + // Include only the minimal necessary functions initialize_app, get_component_config, - set_component_config ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} +} \ No newline at end of file diff --git a/interface/src-tauri/src/timestamp_serde.rs b/interface/src-tauri/src/timestamp_serde.rs new file mode 100644 index 00000000..c04f13af --- /dev/null +++ b/interface/src-tauri/src/timestamp_serde.rs @@ -0,0 +1,38 @@ +// A simple module to help serialize/deserialize prost's Timestamp type +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use prost_types::Timestamp; + +pub fn serialize(timestamp: &Option, serializer: S) -> Result +where + S: Serializer, +{ + match timestamp { + Some(ts) => { + let ts_str = format!("{}.{:09}", ts.seconds, ts.nanos); + serializer.serialize_str(&ts_str) + } + None => serializer.serialize_none(), + } +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let ts_str = Option::::deserialize(deserializer)?; + + match ts_str { + Some(s) => { + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() != 2 { + return Err(serde::de::Error::custom("Invalid timestamp format")); + } + + let seconds = parts[0].parse::().map_err(serde::de::Error::custom)?; + let nanos = parts[1].parse::().map_err(serde::de::Error::custom)?; + + Ok(Some(Timestamp { seconds, nanos })) + } + None => Ok(None), + } +} \ No newline at end of file diff --git a/interface/src/auto_generated_types.ts b/interface/src/auto_generated_types.ts index fb7c5358..c58a3df8 100644 --- a/interface/src/auto_generated_types.ts +++ b/interface/src/auto_generated_types.ts @@ -1,26 +1,11 @@ // Auto‐generated types -export interface CollectorConfig { - id: string; - host: string; - port: number; - timestamp_format: string; - screen: ScreenConfig; - camera: CameraConfig; - microphone: MicrophoneConfig; - processes: ProcessesConfig; - hyprland: HyprlandConfig; -} - -export interface HyprlandConfig { +export interface ScreenConfig { enabled: boolean; interval: number; output_dir: string; - log_clients: boolean; - log_activewindow: boolean; - log_workspace: boolean; - log_active_monitor: boolean; - log_devices: boolean; + program: string; + timestamp_format: string; } export interface TextUploadConfig { @@ -30,20 +15,6 @@ export interface TextUploadConfig { supported_formats: string[]; } -export interface MouseConfig { - enabled: boolean; - interval: number; - output_dir: string; -} - -export interface AmbientConfig { - enabled: boolean; - interval: number; - output_dir: string; - temperature_sensor_path: string | null; - humidity_sensor_path: string | null; -} - export interface InputLoggerConfig { output_dir: string; enabled: boolean; @@ -55,29 +26,37 @@ export interface InputLoggerConfig { mouse_interval: number; } -export interface CollectorState { - name: string; +export interface ScreenFrame { + uuid: string; timestamp: Date; + width: number; + height: number; + image_bytes: Uint8Array; + mime_type: string; } -export interface ScreenConfig { +export interface CameraConfig { enabled: boolean; interval: number; output_dir: string; - program: string; + device: string; + resolution_x: number; + resolution_y: number; + fps: number; timestamp_format: string; } -export interface ServerConfig { - host: string; - port: number; - database_endpoint: string; - database_name: string; - server_name: string; +export interface MicrophoneConfig { + enabled: boolean; + output_dir: string; + sample_rate: number; + chunk_duration_secs: number; + timestamp_format: string; + bits_per_sample: number; + channels: number; + capture_interval_secs: number; } -export type ServerCommand = "RegisterCollector" | "GetConfig" | "SetConfig" | "GetData" | "Query" | "ReportState" | "GetState"; - export interface WeatherConfig { enabled: boolean; interval: number; @@ -87,42 +66,51 @@ export interface WeatherConfig { longitude: number; } -export interface KeyboardConfig { +export interface WifiConfig { enabled: boolean; interval: number; output_dir: string; + scan_command: string; } -export interface CameraConfig { +export interface InterfaceState { +} + +export interface AmbientConfig { enabled: boolean; interval: number; output_dir: string; - device: string; - resolution_x: number; - resolution_y: number; - fps: number; - timestamp_format: string; + temperature_sensor_path: string | null; + humidity_sensor_path: string | null; } -export interface SystemPerformanceConfig { +export interface GeoConfig { enabled: boolean; interval: number; output_dir: string; - log_cpu: boolean; - log_memory: boolean; - log_disk: boolean; + use_ip_fallback: boolean; } -export interface GeoConfig { +export interface CollectorState { + name: string; + timestamp: Date; +} + +export interface SystemPerformanceConfig { enabled: boolean; interval: number; output_dir: string; - use_ip_fallback: boolean; + log_cpu: boolean; + log_memory: boolean; + log_disk: boolean; } -export interface SystemConfig { - server: ServerConfig; - collector: CollectorConfig; +export interface ServerConfig { + host: string; + port: number; + database_endpoint: string; + database_name: string; + server_name: string; } export interface AudioConfig { @@ -132,56 +120,68 @@ export interface AudioConfig { chunk_duration_secs: number; } -export interface MicrophoneConfig { +export interface MouseConfig { enabled: boolean; + interval: number; output_dir: string; - sample_rate: number; - chunk_duration_secs: number; - timestamp_format: string; - bits_per_sample: number; - channels: number; - capture_interval_secs: number; } -export interface WifiConfig { +export interface NetworkConfig { enabled: boolean; interval: number; output_dir: string; - scan_command: string; } -export interface ScreenFrame { - uuid: string; - timestamp: Date; - width: number; - height: number; - image_bytes: Uint8Array; - mime_type: string; +export interface CollectorConfig { + id: string; + host: string; + port: number; + timestamp_format: string; + screen: ScreenConfig; + camera: CameraConfig; + microphone: MicrophoneConfig; + processes: ProcessesConfig; + hyprland: HyprlandConfig; } -export interface ServerState { - name: string; - timestamp: Date; - cpu_usage: number; - memory_usage: number; - threads: number; - pending_commands: any[]; +export type ServerCommand = "RegisterCollector" | "GetConfig" | "SetConfig" | "GetData" | "Query" | "ReportState" | "GetState"; + +export interface SystemConfig { + server: ServerConfig; + collector: CollectorConfig; } +export type DataModality = "Screen"; + export interface ProcessesConfig { enabled: boolean; interval: number; output_dir: string; } -export type DataModality = "Screen"; +export interface HyprlandConfig { + enabled: boolean; + interval: number; + output_dir: string; + log_clients: boolean; + log_activewindow: boolean; + log_workspace: boolean; + log_active_monitor: boolean; + log_devices: boolean; +} -export interface NetworkConfig { +export interface KeyboardConfig { enabled: boolean; interval: number; output_dir: string; } -export interface InterfaceState { +export interface ServerState { + name: string; + timestamp: Date; + cpu_usage: number; + memory_usage: number; + threads: number; + pending_commands: any[]; } diff --git a/interface/src/components/ScreenDashboard.tsx b/interface/src/components/ScreenDashboard.tsx index 8b909be2..68b2d8e1 100644 --- a/interface/src/components/ScreenDashboard.tsx +++ b/interface/src/components/ScreenDashboard.tsx @@ -1,23 +1,20 @@ import { useState, useEffect, useRef } from 'react'; -import { invoke } from '@tauri-apps/api/core'; // Ensure invoke is imported +import { invoke } from '@tauri-apps/api/core'; import { Button } from './ui/button'; import { Camera, X, Settings, Power, Clock, ArrowUpDown, RefreshCw } from 'lucide-react'; import { Slider } from './ui/slider'; import { Switch } from './ui/switch'; -import axios from 'axios'; // Keep axios for fetching image data, remove for config +import axios from 'axios'; -// Server API endpoint for non-config data const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; interface Screenshot { - id: number; // Keep this if server provides it, otherwise use path or timestamp as key + id: number; timestamp: number; path: string; dataUrl?: string; - // Add other fields if available from server like width/height } -// Mirror the ScreenConfig structure from lifelog_types.proto interface ScreenConfig { enabled: boolean; interval: number; @@ -35,22 +32,19 @@ export default function ScreenDashboard() { const [selectedScreenshot, setSelectedScreenshot] = useState(null); const [totalPages, setTotalPages] = useState(1); const [showSettings, setShowSettings] = useState(false); - // Use the ScreenConfig interface for settings state const [settings, setSettings] = useState(null); const [isLoadingSettings, setIsLoadingSettings] = useState(false); const [isSavingSettings, setIsSavingSettings] = useState(false); - // Use separate state for temporary edits in the settings panel - const [tempInterval, setTempInterval] = useState(60); // Default or load from settings - const [tempEnabled, setTempEnabled] = useState(true); // Default or load from settings + const [tempInterval, setTempInterval] = useState(60); + const [tempEnabled, setTempEnabled] = useState(true); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); const [autoRefresh, setAutoRefresh] = useState(false); const refreshIntervalRef = useRef(); const pageSize = 9; - // Initial load effect useEffect(() => { - loadScreenshots(); // Keep loading screenshot list via axios/API - loadSettings(); // Load settings via Tauri invoke + loadScreenshots(); + loadSettings(); const savedAutoRefresh = localStorage.getItem('screenshots_auto_refresh'); if (savedAutoRefresh !== null) { @@ -58,10 +52,9 @@ export default function ScreenDashboard() { } }, []); - // Handle page changes for screenshots useEffect(() => { loadScreenshots(); - }, [currentPage, sortOrder]); // Reload when sort order changes too + }, [currentPage, sortOrder]); // Handle auto-refresh setup/teardown based on settings interval useEffect(() => { @@ -71,15 +64,14 @@ export default function ScreenDashboard() { } if (autoRefresh && settings && settings.enabled && settings.interval > 0) { - console.log(`Setting up auto-refresh interval: ${settings.interval} seconds`); + console.log(`[Screen] Setting up auto-refresh interval: ${settings.interval} seconds`); refreshIntervalRef.current = window.setInterval(() => { - console.log('Auto-refreshing screenshots...'); + console.log('[Screen] Auto-refreshing...'); // Refresh only if currently on the first page and sorting by newest - // to avoid unexpected page jumps if (currentPage === 1 && sortOrder === 'desc') { loadScreenshots(); } else { - console.log("Auto-refresh skipped (not on page 1 or not sorting by newest)"); + console.log("[Screen] Auto-refresh skipped (not page 1 or not newest sort)"); } }, settings.interval * 1000); } @@ -94,93 +86,79 @@ export default function ScreenDashboard() { }; }, [autoRefresh, settings?.interval, settings?.enabled, currentPage, sortOrder]); - // Load screenshot list (remains using axios for now) async function loadScreenshots() { setIsLoading(true); try { - // Fetch screenshot list from API const response = await axios.get(`${API_BASE_URL}/api/logger/screen/data`, { params: { page: currentPage, page_size: pageSize, limit: pageSize, - // Apply sorting based on state ...(sortOrder === 'desc' ? { filter: "ORDER BY timestamp DESC" } : { filter: "ORDER BY timestamp ASC" }) } }); - console.log("Screen frames list loaded:", response.data); + console.log("[Screen] Frames list loaded:", response.data); - // Assuming response.data is an array of screenshot metadata objects - // Map to Screenshot interface, might need adjustments based on actual API response const mappedScreenshots = response.data.map((item: any) => ({ - // Use a unique identifier if available, otherwise generate one or use path/timestamp - id: item.id ?? Math.random(), // Example: Use id if present, otherwise random (not ideal for keys) + id: item.id ?? Math.random(), timestamp: item.timestamp, path: item.path, - // dataUrl will be loaded on demand or preloaded below })); - // Preload image data (optional, can also load onClick) const screenshotsWithData = await Promise.all( mappedScreenshots.map(async (screenshot: Screenshot) => { try { - const imageResponse = await axios.get(`${API_BASE_URL}/api/files/screen/${screenshot.path}`, { // Assuming API structure + const imageResponse = await axios.get(`${API_BASE_URL}/api/files/screen/${screenshot.path}`, { responseType: 'blob' }); const dataUrl = URL.createObjectURL(imageResponse.data); return { ...screenshot, dataUrl }; } catch (error) { - console.error(`Failed to load data for screenshot ${screenshot.path}:`, error); - return screenshot; // Return original object if loading fails + console.error(`[Screen] Failed to load data for ${screenshot.path}:`, error); + return screenshot; } }) ); setScreenshots(screenshotsWithData); - // Calculate total pages (adjust based on how API provides total count) const totalCountHeader = response.headers['x-total-count']; - const totalCount = totalCountHeader ? parseInt(totalCountHeader) : screenshotsWithData.length + (currentPage * pageSize); // Estimate if header missing + const totalCount = totalCountHeader ? parseInt(totalCountHeader) : screenshotsWithData.length + (currentPage * pageSize); setTotalPages(Math.ceil(totalCount / pageSize)); } catch (error) { - console.error('Failed to load screenshots:', error); - // Maybe show an error message to the user + console.error('[Screen] Failed to load screenshots:', error); } finally { setIsLoading(false); } } - // Load settings using Tauri invoke async function loadSettings() { setIsLoadingSettings(true); try { - console.log("Requesting screen config via Tauri..."); - // Use invoke to call the generic backend function + console.log("[Screen] Requesting config via Tauri..."); const result = await invoke("get_component_config", { componentName: "screen" }); - console.log("Received screen config from Tauri:", result); + console.log("[Screen] Received config from Tauri:", result); - // Assuming result is the ScreenConfig object or null if (result && typeof result === 'object') { - // Explicitly cast to ScreenConfig - ensure properties match const loadedSettings = result as ScreenConfig; - // Validate required fields (optional but recommended) if (typeof loadedSettings.enabled !== 'boolean' || typeof loadedSettings.interval !== 'number') { + console.error("[Screen] Invalid settings format from backend.", loadedSettings); throw new Error("Received invalid settings format from backend."); } + console.log("[Screen] Parsed settings successfully:", loadedSettings); setSettings(loadedSettings); setTempInterval(loadedSettings.interval); setTempEnabled(loadedSettings.enabled); } else if (result === null) { - console.warn("Backend returned null for screen config. Using defaults."); - // Handle null case: use default settings or show an error + console.warn("[Screen] Backend returned null config. Using defaults."); const defaultSettings: ScreenConfig = { enabled: false, interval: 60, - output_dir: "", // Provide defaults if needed + output_dir: "", program: "", timestamp_format: "" }; @@ -188,25 +166,24 @@ export default function ScreenDashboard() { setTempInterval(defaultSettings.interval); setTempEnabled(defaultSettings.enabled); } else { + console.error("[Screen] Unexpected data format for settings:", result); throw new Error("Received unexpected data format for settings."); } } catch (error) { - console.error('Failed to load screen settings via Tauri:', error); - // Optionally set default settings or show an error message + console.error('[Screen] Failed to load settings via Tauri:', error); const defaultSettings: ScreenConfig = { enabled: false, interval: 60, output_dir: "", program: "", timestamp_format: "" }; - setSettings(defaultSettings); // Fallback to default + setSettings(defaultSettings); setTempInterval(defaultSettings.interval); setTempEnabled(defaultSettings.enabled); - alert(`Failed to load settings: ${error}`); // Inform user + alert(`Failed to load settings: ${error}`); } finally { setIsLoadingSettings(false); } } - // Save settings using Tauri invoke async function saveSettings() { if (!settings) { alert("Cannot save settings: Current settings not loaded."); @@ -224,46 +201,46 @@ export default function ScreenDashboard() { setIsSavingSettings(true); try { - // Construct the updated config object based on temp state - // Make sure to include all fields expected by the ScreenConfig struct const updatedConfig: ScreenConfig = { - ...settings, // Start with existing settings (includes output_dir etc.) + ...settings, enabled: tempEnabled, interval: tempInterval, - // Ensure other fields from 'settings' are included if they exist output_dir: settings.output_dir || "", program: settings.program || "", timestamp_format: settings.timestamp_format || "" }; - console.log("Saving screen config via Tauri:", updatedConfig); + console.log("[Screen] Saving config via Tauri:", updatedConfig); - // Use invoke to call the generic backend function await invoke("set_component_config", { componentName: "screen", - config: updatedConfig // Pass the complete object + config: updatedConfig }); - console.log("Screen settings saved successfully via Tauri."); + console.log("[Screen] Settings saved successfully via Tauri."); setSettings(updatedConfig); setShowSettings(false); + if (wasAutoRefreshEnabled) { + setTimeout(() => { + setAutoRefresh(true); + }, 1000); // Delay to ensure settings take effect + } + + if (wasAutoRefreshEnabled && updatedConfig.enabled && updatedConfig.interval > 0) { + console.log(`[Screen] Updated auto-refresh interval: ${updatedConfig.interval} seconds`); + } + } catch (error) { - console.error('Failed to save settings via Tauri:', error); + console.error('[Screen] Failed to save settings via Tauri:', error); alert(`Failed to save settings: ${error}`); } finally { setIsSavingSettings(false); - if (wasAutoRefreshEnabled) { - setTimeout(() => setAutoRefresh(true), 500); - } } } - - // --- Helper functions (formatTimestamp, handlePreviousPage, etc.) remain largely the same --- function formatTimestamp(timestamp: number): string { - // Multiply by 1000 if timestamp is in seconds const date = new Date(timestamp * 1000); return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', @@ -284,10 +261,9 @@ export default function ScreenDashboard() { } function handleScreenshotClick(screenshot: Screenshot) { - console.log("Opening screenshot:", screenshot); - // Use the dataUrl if available, otherwise construct API path + console.log("[Screen] Opening screenshot:", screenshot); const imageUrl = screenshot.dataUrl || `${API_BASE_URL}/api/files/screen/${screenshot.path}`; - setSelectedImage(imageUrl); // Store the URL to display + setSelectedImage(imageUrl); setSelectedScreenshot(screenshot); } @@ -298,7 +274,6 @@ export default function ScreenDashboard() { function toggleSettings() { setShowSettings(!showSettings); - // Reset temp values from actual sttings when opening if (!showSettings && settings) { setTempInterval(settings.interval); setTempEnabled(settings.enabled); @@ -321,11 +296,8 @@ export default function ScreenDashboard() { function toggleSortOrder() { setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc')); - // Optionally reset to page 1 when changing sort order - // setCurrentPage(1); } - // Re-sort screenshots whenever screenshots or sortOrder changes const sortedScreenshots = [...screenshots].sort((a, b) => { return sortOrder === 'asc' ? a.timestamp - b.timestamp : b.timestamp - a.timestamp; }); @@ -344,7 +316,7 @@ export default function ScreenDashboard() { onClick={toggleSettings} variant="secondary" className="flex items-center gap-2" - disabled={isLoadingSettings} // Disable while loading initial settings + disabled={isLoadingSettings} > {isLoadingSettings ? ( @@ -399,12 +371,12 @@ export default function ScreenDashboard() {
setTempInterval(values[0])} - disabled={!tempEnabled} // Disable slider if capture is off + disabled={!tempEnabled} className="data-[disabled]:opacity-50" />
@@ -465,7 +437,7 @@ export default function ScreenDashboard() { id="auto-refresh" checked={autoRefresh} onChange={(e) => setAutoRefresh(e.target.checked)} - disabled={!settings || !settings.enabled} // Disable if logger off + disabled={!settings || !settings.enabled} className="h-4 w-4 text-[#4C8BF5] rounded focus:ring-2 focus:ring-[#4C8BF5]/20 bg-[#232B3D] border-[#2A3142] disabled:opacity-50" />
@@ -568,9 +535,8 @@ export default function ScreenDashboard() { className="relative max-w-7xl max-h-[90vh] bg-[#1C2233] rounded-lg shadow-2xl overflow-hidden" onClick={(e) => e.stopPropagation()} > - {/* Consider adding loading state for full image */} {`Full diff --git a/proto/lifelog_types.proto b/proto/lifelog_types.proto index 03acaf50..e2721c8f 100644 --- a/proto/lifelog_types.proto +++ b/proto/lifelog_types.proto @@ -5,27 +5,12 @@ import "google/protobuf/timestamp.proto"; import "google/protobuf/any.proto"; import "google/protobuf/wrappers.proto"; -message CollectorConfig { - string id = 1; - string host = 2; - uint32 port = 3; - string timestamp_format = 4; - ScreenConfig screen = 5; - CameraConfig camera = 6; - MicrophoneConfig microphone = 7; - ProcessesConfig processes = 8; - HyprlandConfig hyprland = 9; -} - -message HyprlandConfig { +message ScreenConfig { bool enabled = 1; double interval = 2; string output_dir = 3; - bool log_clients = 4; - bool log_activewindow = 5; - bool log_workspace = 6; - bool log_active_monitor = 7; - bool log_devices = 8; + string program = 4; + string timestamp_format = 5; } message TextUploadConfig { @@ -35,20 +20,6 @@ message TextUploadConfig { repeated string supported_formats = 4; } -message MouseConfig { - bool enabled = 1; - double interval = 2; - string output_dir = 3; -} - -message AmbientConfig { - bool enabled = 1; - double interval = 2; - string output_dir = 3; - google.protobuf.StringValue temperature_sensor_path = 4; - google.protobuf.StringValue humidity_sensor_path = 5; -} - message InputLoggerConfig { string output_dir = 1; bool enabled = 2; @@ -60,35 +31,35 @@ message InputLoggerConfig { double mouse_interval = 8; } -message CollectorState { - string name = 1; +message ScreenFrame { + string uuid = 1; google.protobuf.Timestamp timestamp = 2; + uint32 width = 3; + uint32 height = 4; + bytes image_bytes = 5; + string mime_type = 6; } -message ScreenConfig { +message CameraConfig { bool enabled = 1; double interval = 2; string output_dir = 3; - string program = 4; - string timestamp_format = 5; -} - -message ServerConfig { - string host = 1; - uint32 port = 2; - string database_endpoint = 3; - string database_name = 4; - string server_name = 5; + string device = 4; + uint32 resolution_x = 5; + uint32 resolution_y = 6; + uint32 fps = 7; + string timestamp_format = 8; } -enum ServerCommand { - RegisterCollector = 0; - GetConfig = 1; - SetConfig = 2; - GetData = 3; - Query = 4; - ReportState = 5; - GetState = 6; +message MicrophoneConfig { + bool enabled = 1; + string output_dir = 2; + uint32 sample_rate = 3; + uint64 chunk_duration_secs = 4; + string timestamp_format = 5; + uint32 bits_per_sample = 6; + uint32 channels = 7; + uint64 capture_interval_secs = 8; } message WeatherConfig { @@ -100,42 +71,51 @@ message WeatherConfig { double longitude = 6; } -message KeyboardConfig { +message WifiConfig { bool enabled = 1; double interval = 2; string output_dir = 3; + string scan_command = 4; } -message CameraConfig { +message InterfaceState { +} + +message AmbientConfig { bool enabled = 1; double interval = 2; string output_dir = 3; - string device = 4; - uint32 resolution_x = 5; - uint32 resolution_y = 6; - uint32 fps = 7; - string timestamp_format = 8; + google.protobuf.StringValue temperature_sensor_path = 4; + google.protobuf.StringValue humidity_sensor_path = 5; } -message SystemPerformanceConfig { +message GeoConfig { bool enabled = 1; double interval = 2; string output_dir = 3; - bool log_cpu = 4; - bool log_memory = 5; - bool log_disk = 6; + bool use_ip_fallback = 4; } -message GeoConfig { +message CollectorState { + string name = 1; + google.protobuf.Timestamp timestamp = 2; +} + +message SystemPerformanceConfig { bool enabled = 1; double interval = 2; string output_dir = 3; - bool use_ip_fallback = 4; + bool log_cpu = 4; + bool log_memory = 5; + bool log_disk = 6; } -message SystemConfig { - ServerConfig server = 1; - CollectorConfig collector = 2; +message ServerConfig { + string host = 1; + uint32 port = 2; + string database_endpoint = 3; + string database_name = 4; + string server_name = 5; } message AudioConfig { @@ -145,40 +125,47 @@ message AudioConfig { uint64 chunk_duration_secs = 4; } -message MicrophoneConfig { +message MouseConfig { bool enabled = 1; - string output_dir = 2; - uint32 sample_rate = 3; - uint64 chunk_duration_secs = 4; - string timestamp_format = 5; - uint32 bits_per_sample = 6; - uint32 channels = 7; - uint64 capture_interval_secs = 8; + double interval = 2; + string output_dir = 3; } -message WifiConfig { +message NetworkConfig { bool enabled = 1; double interval = 2; string output_dir = 3; - string scan_command = 4; } -message ScreenFrame { - string uuid = 1; - google.protobuf.Timestamp timestamp = 2; - uint32 width = 3; - uint32 height = 4; - bytes image_bytes = 5; - string mime_type = 6; +message CollectorConfig { + string id = 1; + string host = 2; + uint32 port = 3; + string timestamp_format = 4; + ScreenConfig screen = 5; + CameraConfig camera = 6; + MicrophoneConfig microphone = 7; + ProcessesConfig processes = 8; + HyprlandConfig hyprland = 9; } -message ServerState { - string name = 1; - google.protobuf.Timestamp timestamp = 2; - float cpu_usage = 3; - float memory_usage = 4; - float threads = 5; - repeated string pending_commands = 6; +enum ServerCommand { + RegisterCollector = 0; + GetConfig = 1; + SetConfig = 2; + GetData = 3; + Query = 4; + ReportState = 5; + GetState = 6; +} + +message SystemConfig { + ServerConfig server = 1; + CollectorConfig collector = 2; +} + +enum DataModality { + Screen = 0; } message ProcessesConfig { @@ -187,17 +174,30 @@ message ProcessesConfig { string output_dir = 3; } -enum DataModality { - Screen = 0; +message HyprlandConfig { + bool enabled = 1; + double interval = 2; + string output_dir = 3; + bool log_clients = 4; + bool log_activewindow = 5; + bool log_workspace = 6; + bool log_active_monitor = 7; + bool log_devices = 8; } -message NetworkConfig { +message KeyboardConfig { bool enabled = 1; double interval = 2; string output_dir = 3; } -message InterfaceState { +message ServerState { + string name = 1; + google.protobuf.Timestamp timestamp = 2; + float cpu_usage = 3; + float memory_usage = 4; + float threads = 5; + repeated string pending_commands = 6; } message LifelogData {