vibetunnel/tauri/src-tauri/src/main.rs
Peter Steinberger 2b611a5797 Work on Tauri
2025-06-23 17:16:29 +02:00

1041 lines
38 KiB
Rust

#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use tauri::menu::Menu;
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Emitter, Manager, WindowEvent};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod api_client;
mod api_testing;
mod app_mover;
mod auth_cache;
mod auto_launch;
mod backend_manager;
mod cli_installer;
mod commands;
mod debug_features;
mod errors;
mod fs_api;
mod keychain;
mod network_utils;
mod ngrok;
mod notification_manager;
mod permissions;
mod port_conflict;
mod session_monitor;
mod settings;
mod state;
mod terminal;
mod terminal_detector;
mod terminal_integrations;
mod terminal_spawn_service;
mod tray_menu;
mod tty_forward;
#[cfg(unix)]
mod unix_socket_server;
mod updater;
mod welcome;
use commands::ServerStatus;
use commands::*;
use state::AppState;
#[tauri::command]
fn open_settings_window(app: AppHandle, tab: Option<String>) -> Result<(), String> {
tracing::info!("Opening settings window");
// Build URL with optional tab parameter
let url = if let Some(tab_name) = tab {
format!("settings.html?tab={}", tab_name)
} else {
"settings.html".to_string()
};
// Check if settings window already exists
if let Some(window) = app.get_webview_window("settings") {
// Navigate to the URL with the tab parameter if window exists
window
.eval(&format!("window.location.href = '{}'", url))
.map_err(|e| e.to_string())?;
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
} else {
// Create new settings window
tracing::info!("Creating new settings window with URL: {}", url);
let window =
tauri::WebviewWindowBuilder::new(&app, "settings", tauri::WebviewUrl::App(url.into()))
.title("VibeTunnel Settings")
.inner_size(1200.0, 800.0)
.resizable(true)
.decorations(true)
.center()
.build()
.map_err(|e| {
tracing::error!("Failed to create settings window: {}", e);
e.to_string()
})?;
tracing::info!("Settings window created successfully");
// Handle close event to destroy the window
let window_clone = window.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { .. } = event {
let _ = window_clone.close();
}
});
}
Ok(())
}
#[tauri::command]
fn focus_terminal_window(session_id: String) -> Result<(), String> {
// Focus the terminal window for the given session
#[cfg(target_os = "macos")]
{
use std::process::Command;
// Use AppleScript to focus the terminal window
let script = format!(
r#"tell application "System Events"
set allProcesses to name of every process
if "Terminal" is in allProcesses then
tell application "Terminal"
activate
repeat with w in windows
if name of w contains "{}" then
set index of w to 1
return
end if
end repeat
end tell
end if
end tell"#,
session_id
);
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()
.map_err(|e| format!("Failed to execute AppleScript: {}", e))?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
return Err(format!("AppleScript failed: {}", error));
}
}
#[cfg(not(target_os = "macos"))]
{
// On other platforms, we can try to use wmctrl or similar tools
// For now, just return an error
return Err("Terminal window focus not implemented for this platform".to_string());
}
Ok(())
}
#[tauri::command]
fn open_session_detail_window(app: AppHandle, session_id: String) -> Result<(), String> {
// Build URL with session ID parameter
let url = format!("session-detail.html?id={}", session_id);
let window_id = format!("session-detail-{}", session_id);
// Check if session detail window already exists for this session
if let Some(window) = app.get_webview_window(&window_id) {
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
} else {
// Create new session detail window
let window =
tauri::WebviewWindowBuilder::new(&app, window_id, tauri::WebviewUrl::App(url.into()))
.title("Session Details")
.inner_size(600.0, 450.0)
.resizable(true)
.decorations(true)
.center()
.build()
.map_err(|e| e.to_string())?;
// Handle close event to destroy the window
let window_clone = window.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { .. } = event {
let _ = window_clone.close();
}
});
}
Ok(())
}
fn update_tray_menu_status(app: &AppHandle, port: u16, session_count: usize) {
// Update tray menu status using the tray menu manager
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
tray_menu::TrayMenuManager::update_server_status(&app_handle, port, true).await;
tray_menu::TrayMenuManager::update_session_count(&app_handle, session_count).await;
});
}
fn main() {
// Initialize tracing
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "vibetunnel=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tauri::Builder::default()
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_single_instance::init(|_app, _args, _cwd| {
// Don't show main window on startup - app runs in system tray
// let _ = show_main_window(app.app_handle().clone());
}))
.manage(AppState::new())
.invoke_handler(tauri::generate_handler![
create_terminal,
list_terminals,
close_terminal,
resize_terminal,
write_to_terminal,
read_from_terminal,
start_server,
stop_server,
get_server_status,
get_app_version,
get_os,
restart_server,
show_server_console,
show_welcome_screen,
purge_all_settings,
update_dock_icon_visibility,
show_main_window,
open_settings_window,
open_session_detail_window,
focus_terminal_window,
quit_app,
settings::get_settings,
settings::save_settings,
auto_launch::set_auto_launch,
auto_launch::get_auto_launch,
ngrok::start_ngrok_tunnel,
ngrok::stop_ngrok_tunnel,
ngrok::get_ngrok_status,
terminal_detector::detect_system_terminals,
terminal_detector::get_default_shell,
cli_installer::install_cli,
cli_installer::uninstall_cli,
cli_installer::check_cli_installed,
start_tty_forward,
stop_tty_forward,
list_tty_forwards,
get_tty_forward,
get_session_stats,
get_monitored_sessions,
start_session_monitoring,
check_port_availability,
detect_port_conflict,
resolve_port_conflict,
force_kill_process,
find_available_ports,
get_local_ip_address,
get_all_ip_addresses,
get_network_interfaces,
get_hostname,
test_network_connectivity,
get_network_stats,
show_notification,
get_notifications,
get_notification_history,
mark_notification_as_read,
mark_all_notifications_as_read,
clear_notification,
clear_all_notifications,
get_unread_notification_count,
update_notification_settings,
get_notification_settings,
get_welcome_state,
should_show_welcome,
get_tutorials,
get_tutorial_category,
complete_tutorial_step,
skip_tutorial,
reset_tutorial,
get_tutorial_progress,
show_welcome_window,
get_all_advanced_settings,
update_advanced_settings,
reset_settings_section,
export_settings,
import_settings,
check_all_permissions,
check_permission,
check_permission_silent,
request_permission,
get_permission_info,
get_all_permissions,
get_required_permissions,
get_missing_required_permissions,
all_required_permissions_granted,
open_system_permission_settings,
get_permission_stats,
check_for_updates,
download_update,
install_update,
cancel_update,
get_update_state,
get_updater_settings,
update_updater_settings,
switch_update_channel,
get_update_history,
// Multi-backend commands not applicable to Node.js subprocess
// get_available_backends,
// get_backend_config,
// is_backend_installed,
// install_backend,
// start_backend,
// stop_backend,
// switch_backend,
// get_active_backend,
// get_backend_instances,
// check_backend_health,
// get_backend_stats,
get_debug_settings,
update_debug_settings,
log_debug_message,
record_performance_metric,
take_memory_snapshot,
get_debug_logs,
get_performance_metrics,
get_memory_snapshots,
get_network_requests,
run_api_tests,
run_benchmarks,
generate_diagnostic_report,
clear_debug_data,
set_debug_mode,
get_debug_stats,
get_api_test_config,
update_api_test_config,
add_api_test_suite,
get_api_test_suite,
list_api_test_suites,
run_single_api_test,
run_api_test_suite,
get_api_test_history,
clear_api_test_history,
import_postman_collection,
export_api_test_suite,
get_auth_cache_config,
update_auth_cache_config,
store_auth_token,
get_auth_token,
store_auth_credential,
get_auth_credential,
clear_auth_cache_entry,
clear_all_auth_cache,
get_auth_cache_stats,
list_auth_cache_entries,
export_auth_cache,
import_auth_cache,
hash_password,
create_auth_cache_key,
detect_installed_terminals,
get_default_terminal,
set_default_terminal,
launch_terminal_emulator,
get_terminal_config,
update_terminal_config,
list_detected_terminals,
create_terminal_ssh_url,
get_terminal_integration_stats,
// Settings UI Commands
get_all_settings,
update_setting,
set_dashboard_password,
restart_server_with_port,
update_server_bind_address,
set_dock_icon_visibility,
set_log_level,
test_api_endpoint,
get_server_logs,
export_logs,
get_local_ip,
detect_terminals,
// App Mover Commands
app_mover::prompt_move_to_applications,
app_mover::is_in_applications_folder_command,
// Terminal Spawn Service Commands
terminal_spawn_service::spawn_terminal_for_session,
terminal_spawn_service::spawn_terminal_with_command,
terminal_spawn_service::spawn_custom_terminal,
// Keychain Commands
keychain_set_password,
keychain_get_password,
keychain_delete_password,
keychain_set_dashboard_password,
keychain_get_dashboard_password,
keychain_delete_dashboard_password,
keychain_set_ngrok_auth_token,
keychain_get_ngrok_auth_token,
keychain_delete_ngrok_auth_token,
keychain_list_keys,
// Welcome flow commands
request_all_permissions,
test_terminal,
check_vt_installation,
install_vt,
check_permissions,
request_automation_permission,
request_accessibility_permission,
save_dashboard_password,
open_dashboard,
finish_welcome,
])
.setup(|app| {
// Set app handle in managers
let state_clone = app.state::<AppState>().inner().clone();
let app_handle = app.handle().clone();
let app_handle2 = app.handle().clone();
let app_handle3 = app.handle().clone();
let app_handle4 = app.handle().clone();
let app_handle_for_move = app.handle().clone();
tauri::async_runtime::spawn(async move {
let state = state_clone;
state.notification_manager.set_app_handle(app_handle).await;
state.welcome_manager.set_app_handle(app_handle2).await;
state.permissions_manager.set_app_handle(app_handle3).await;
state.update_manager.set_app_handle(app_handle4).await;
// Start background workers now that we have a runtime
state.terminal_spawn_service.clone().start_worker().await;
state.auth_cache_manager.start_cleanup_task().await;
// Start Unix socket server for terminal spawning (macOS/Linux)
#[cfg(unix)]
{
if let Err(e) = state.unix_socket_server.start() {
tracing::error!("Failed to start Unix socket server: {}", e);
}
}
// Start session monitoring
state.session_monitor.start_monitoring().await;
// Load welcome state and check if should show welcome
let _ = state.welcome_manager.load_state().await;
if state.welcome_manager.should_show_welcome().await {
let _ = state.welcome_manager.show_welcome_window().await;
}
// Check permissions on startup
let _ = state.permissions_manager.check_all_permissions().await;
// Check if app should be moved to Applications folder (macOS only)
#[cfg(target_os = "macos")]
{
let app_handle_move = app_handle_for_move.clone();
tokio::spawn(async move {
// Small delay to let the app fully initialize
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let _ = app_mover::check_and_prompt_move(app_handle_move).await;
});
}
// Load updater settings and start auto-check
let _ = state.update_manager.load_settings().await;
state.update_manager.clone().start_auto_check().await;
});
// Create system tray icon using tray-icon.png for macOS (menu-bar-icon.png is for Windows/Linux)
let tray_icon = if let Ok(resource_dir) = app.path().resource_dir() {
// On macOS, use tray-icon.png which has the proper design for the menu bar
let icon_name = if cfg!(target_os = "macos") {
"tray-icon.png"
} else {
"menu-bar-icon.png"
};
let icon_path = resource_dir.join(icon_name);
if let Ok(icon_data) = std::fs::read(&icon_path) {
tauri::image::Image::from_bytes(&icon_data).ok()
} else {
// Try alternative path
let icon_path2 = resource_dir.join("icons").join(icon_name);
if let Ok(icon_data) = std::fs::read(&icon_path2) {
tauri::image::Image::from_bytes(&icon_data).ok()
} else {
// Fallback to default icon
app.default_window_icon().cloned()
}
}
} else {
// Fallback to default icon if resource dir not found
app.default_window_icon().cloned()
};
if let Some(icon) = tray_icon {
// Create enhanced tray menu
let menu = tray_menu::TrayMenuManager::create_menu(&app.handle())?;
// Build tray icon with template mode for macOS
let _tray = TrayIconBuilder::with_id("main")
.icon(icon)
.icon_as_template(true) // Enable template mode for proper macOS tinting
.tooltip("VibeTunnel")
.menu(&menu)
.on_menu_event(move |app, event| {
handle_tray_menu_event(app, event.id.as_ref());
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
// Get server status and open dashboard in browser
let app = tray.app_handle();
let state = app.state::<AppState>();
if state.backend_manager.blocking_is_running() {
let settings = crate::settings::Settings::load().unwrap_or_default();
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
let _ = open::that(url);
}
}
})
.build(app)?;
}
// Load settings to determine initial dock icon visibility
let settings = settings::Settings::load().unwrap_or_default();
// Set initial dock icon visibility on macOS
#[cfg(target_os = "macos")]
{
// Force dock icon to be visible for debugging
app.set_activation_policy(tauri::ActivationPolicy::Regular);
// if !settings.general.show_dock_icon {
// app.set_activation_policy(tauri::ActivationPolicy::Accessory);
// }
}
// Show settings window for debugging
#[cfg(debug_assertions)]
{
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
// Wait a bit for the app to fully initialize
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
// Open settings window
if let Err(e) = open_settings_window(app_handle, None) {
tracing::error!("Failed to open settings window: {}", e);
}
});
}
// Auto-start server with monitoring
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
tracing::info!("Starting server with monitoring...");
start_server_with_monitoring(app_handle).await;
});
Ok(())
})
.on_menu_event(handle_menu_event)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(target_os = "macos")]
#[allow(dead_code)]
fn create_app_menu(app: &tauri::App) -> Result<Menu<tauri::Wry>, tauri::Error> {
// Create the menu using the builder pattern
let menu = Menu::new(app)?;
// For now, return a basic menu
// TODO: Once we understand the correct Tauri v2 menu API, implement full menu
Ok(menu)
}
fn handle_tray_menu_event(app: &AppHandle, event_id: &str) {
match event_id {
"dashboard" => {
// Get server status and open dashboard in browser
let state = app.state::<AppState>();
let backend_manager = state.backend_manager.clone();
tauri::async_runtime::spawn(async move {
if backend_manager.is_running().await {
let settings = crate::settings::Settings::load().unwrap_or_default();
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
let _ = open::that(url);
}
});
}
"show_tutorial" => {
// Show welcome window instead
let state = app.state::<AppState>();
let welcome_manager = state.welcome_manager.clone();
tauri::async_runtime::spawn(async move {
let _ = welcome_manager.show_welcome_window().await;
});
}
"website" => {
let _ = open::that("https://vibetunnel.sh");
}
"report_issue" => {
let _ = open::that("https://github.com/amantus-ai/vibetunnel/issues");
}
"check_updates" => {
// TODO: Implement update check
tracing::info!("Check for updates");
}
"about" => {
// TODO: Show about dialog
tracing::info!("About VibeTunnel");
}
"settings" => {
// Open native settings window
let _ = open_settings_window(app.clone(), None);
}
"quit" => {
quit_app(app.clone());
}
_ => {
// Handle session clicks (format: "session_<id>")
if event_id.starts_with("session_") {
let session_id = event_id.strip_prefix("session_").unwrap_or("");
if !session_id.is_empty() {
// Open session detail window
let _ = open_session_detail_window(app.clone(), session_id.to_string());
}
}
}
}
}
fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) {
match event.id.as_ref() {
"settings" => {
// Open native settings window instead of main window
let _ = open_settings_window(app.clone(), None);
}
"new-terminal" => {
// Terminal creation should be done via the web dashboard
// Open dashboard in browser
let state = app.state::<AppState>();
let backend_manager = state.backend_manager.clone();
tauri::async_runtime::spawn(async move {
if backend_manager.is_running().await {
let settings = crate::settings::Settings::load().unwrap_or_default();
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
let _ = open::that(url);
}
});
}
"reload" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.eval("window.location.reload()");
}
}
"show-dashboard" => {
// Open dashboard in browser instead of showing main window
let state = app.state::<AppState>();
let backend_manager = state.backend_manager.clone();
tauri::async_runtime::spawn(async move {
if backend_manager.is_running().await {
let settings = crate::settings::Settings::load().unwrap_or_default();
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
let _ = open::that(url);
}
});
}
"quit" => {
quit_app(app.clone());
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"minimize" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.minimize();
}
}
"zoom" => {
if let Some(window) = app.get_webview_window("main") {
// Toggle maximize state
if window.is_maximized().unwrap_or(false) {
let _ = window.unmaximize();
} else {
let _ = window.maximize();
}
}
}
"fullscreen" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_fullscreen(!window.is_fullscreen().unwrap_or(false));
}
}
"close-window" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.close();
}
}
"cut" | "copy" | "paste" | "select-all" | "undo" | "redo" => {
// These are handled by the system automatically for text fields
// For terminal, we'll emit events
if let Some(window) = app.get_webview_window("main") {
let _ = window.emit(&format!("menu:{}", event.id.as_ref()), ());
}
}
_ => {}
}
}
#[tauri::command]
fn show_main_window(app: AppHandle) -> Result<(), String> {
let window = if let Some(window) = app.get_webview_window("main") {
window
} else {
// Create main window if it doesn't exist
tauri::WebviewWindowBuilder::new(&app, "main", tauri::WebviewUrl::App("index.html".into()))
.title("VibeTunnel")
.inner_size(1200.0, 800.0)
.center()
.resizable(true)
.decorations(true)
.build()
.map_err(|e| e.to_string())?
};
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
// Show dock icon on macOS when window is shown
#[cfg(target_os = "macos")]
{
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
}
// Handle window close event to hide instead of quit
let window_clone = window.clone();
let app_clone = app.clone();
window.on_window_event(move |event| {
if let WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window_clone.hide();
// Hide dock icon on macOS when window is hidden (only if settings say so)
#[cfg(target_os = "macos")]
{
if let Ok(settings) = settings::Settings::load() {
if !settings.general.show_dock_icon {
let _ = app_clone.set_activation_policy(tauri::ActivationPolicy::Accessory);
}
}
}
}
});
Ok(())
}
#[tauri::command]
fn quit_app(app: AppHandle) {
// Stop monitoring before exit
let state = app.state::<AppState>();
state
.server_monitoring
.store(false, std::sync::atomic::Ordering::Relaxed);
// Close all terminal sessions
let terminal_manager = state.terminal_manager.clone();
tauri::async_runtime::block_on(async move {
let _ = terminal_manager.close_all_sessions().await;
});
app.exit(0);
}
async fn start_server_with_monitoring(app_handle: AppHandle) {
let state = app_handle.state::<AppState>();
let state_clone = state.inner().clone();
// Start initial server
match start_server_internal(&*state).await {
Ok(status) => {
tracing::info!("Server started on port {}", status.port);
*state.server_target_port.write().await = Some(status.port);
// Update tray menu with server status
update_tray_menu_status(&app_handle, status.port, 0);
// Show notification
let _ = state
.notification_manager
.notify_server_status(true, status.port)
.await;
}
Err(e) => {
tracing::error!("Failed to start server: {}", e);
let _ = state
.notification_manager
.notify_error(
"Server Start Failed",
&format!("Failed to start server: {}", e),
)
.await;
}
}
// Monitor server health
let monitoring_state = state_clone.clone();
let monitoring_app = app_handle.clone();
tauri::async_runtime::spawn(async move {
let mut check_interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
while monitoring_state
.server_monitoring
.load(std::sync::atomic::Ordering::Relaxed)
{
check_interval.tick().await;
// Check if server is still running
let server_running = monitoring_state.backend_manager.is_running().await;
if server_running {
// Perform health check
let health_check_result = perform_server_health_check(&monitoring_state).await;
if !health_check_result {
tracing::warn!("Server health check failed, attempting restart...");
// Stop current server
let _ = stop_server_internal(&monitoring_state).await;
// Wait a bit before restart
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// Restart server
match start_server_internal(&monitoring_state).await {
Ok(status) => {
tracing::info!("Server restarted on port {}", status.port);
*monitoring_state.server_target_port.write().await = Some(status.port);
// Update tray menu with server status
update_tray_menu_status(&monitoring_app, status.port, 0);
// Notify frontend of server restart
if let Some(window) = monitoring_app.get_webview_window("main") {
let _ = window.emit("server:restarted", &status);
}
// Show notification
let _ = monitoring_state
.notification_manager
.notify_server_status(true, status.port)
.await;
}
Err(e) => {
tracing::error!("Failed to restart server: {}", e);
let _ = monitoring_state
.notification_manager
.notify_error(
"Server Restart Failed",
&format!("Failed to restart server: {}", e),
)
.await;
}
}
}
} else {
// Server is not running, attempt to start it
let target_port = *monitoring_state.server_target_port.read().await;
if target_port.is_some() {
tracing::info!("Server not running, attempting to start...");
match start_server_internal(&monitoring_state).await {
Ok(status) => {
tracing::info!("Server started on port {}", status.port);
// Notify frontend of server restart
if let Some(window) = monitoring_app.get_webview_window("main") {
let _ = window.emit("server:restarted", &status);
}
// Show notification
let _ = monitoring_state
.notification_manager
.notify_server_status(true, status.port)
.await;
}
Err(e) => {
tracing::error!("Failed to start server: {}", e);
let _ = monitoring_state
.notification_manager
.notify_error(
"Server Start Failed",
&format!("Failed to start server: {}", e),
)
.await;
}
}
}
}
}
tracing::info!("Server monitoring stopped");
});
}
async fn perform_server_health_check(state: &AppState) -> bool {
// Try to get server status
match get_server_status_internal(state).await {
Ok(status) if status.running => {
// Server reports as running, perform additional check
// by trying to access the API endpoint
let url = format!("http://127.0.0.1:{}/api/sessions", status.port);
match reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(2))
.send()
.await
{
Ok(response) => response.status().is_success(),
Err(_) => false,
}
}
_ => false,
}
}
// Internal server management functions that work directly with AppState
async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String> {
// Check if backend is already running
if state.backend_manager.is_running().await {
// Get port from settings
let settings = crate::settings::Settings::load().unwrap_or_default();
let port = settings.dashboard.server_port;
// Check if ngrok is active
let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() {
ngrok_tunnel.url
} else {
match settings.dashboard.access_mode.as_str() {
"network" => format!("http://0.0.0.0:{}", port),
_ => format!("http://127.0.0.1:{}", port),
}
};
return Ok(ServerStatus {
running: true,
port,
url,
});
}
// Start the Node.js backend
state.backend_manager.start().await?;
// Load settings for access mode
let settings = crate::settings::Settings::load().unwrap_or_default();
let port = settings.dashboard.server_port;
// Handle access mode
let url = match settings.dashboard.access_mode.as_str() {
"network" => {
// Node.js server handles network binding internally
format!("http://0.0.0.0:{}", port)
}
"ngrok" => {
// Try to start ngrok tunnel if auth token is configured
if let Some(auth_token) = settings.advanced.ngrok_auth_token {
if !auth_token.is_empty() {
match state
.ngrok_manager
.start_tunnel(port, Some(auth_token))
.await
{
Ok(tunnel) => tunnel.url,
Err(e) => {
tracing::error!("Failed to start ngrok tunnel: {}", e);
return Err(format!("Failed to start ngrok tunnel: {}", e));
}
}
} else {
return Err("Ngrok auth token is required for ngrok access mode".to_string());
}
} else {
return Err("Ngrok auth token is required for ngrok access mode".to_string());
}
}
_ => {
format!("http://127.0.0.1:{}", port)
}
};
Ok(ServerStatus {
running: true,
port,
url,
})
}
async fn stop_server_internal(state: &AppState) -> Result<(), String> {
// Stop the Node.js backend
state.backend_manager.stop().await?;
// Also stop ngrok tunnel if active
let _ = state.ngrok_manager.stop_tunnel().await;
Ok(())
}
async fn get_server_status_internal(state: &AppState) -> Result<ServerStatus, String> {
if state.backend_manager.is_running().await {
// Get port from settings
let settings = crate::settings::Settings::load().unwrap_or_default();
let port = settings.dashboard.server_port;
// Check if ngrok is active and return its URL
let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() {
ngrok_tunnel.url
} else {
// Check settings to determine the correct URL format
match settings.dashboard.access_mode.as_str() {
"network" => format!("http://0.0.0.0:{}", port),
_ => format!("http://127.0.0.1:{}", port),
}
};
Ok(ServerStatus {
running: true,
port,
url,
})
} else {
Ok(ServerStatus {
running: false,
port: 0,
url: String::new(),
})
}
}