diff --git a/src/rtsp/service.rs b/src/rtsp/service.rs index be7526c9..f7a754cb 100644 --- a/src/rtsp/service.rs +++ b/src/rtsp/service.rs @@ -88,9 +88,10 @@ impl RtspService { let status = self.status.clone(); let client_handles = self.client_handles.clone(); + *self.status.write().await = RtspServiceStatus::Running; + let handle = tokio::spawn(async move { tracing::info!("RTSP service listening on {}", bind_addr); - *status.write().await = RtspServiceStatus::Running; loop { tokio::select! { diff --git a/src/web/handlers/config/rtsp.rs b/src/web/handlers/config/rtsp.rs index 0a70d0bb..7cb4f549 100644 --- a/src/web/handlers/config/rtsp.rs +++ b/src/web/handlers/config/rtsp.rs @@ -53,3 +53,59 @@ pub async fn update_rtsp_config( Ok(Json(RtspConfigResponse::from(&new_config))) } + +pub async fn start_rtsp_service( + State(state): State>, +) -> Result> { + let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?; + let current_config = state.config.get().rtsp.clone(); + let mut start_config = current_config.clone(); + start_config.enabled = true; + + apply_rtsp_config( + &state, + ¤t_config, + &start_config, + ConfigApplyOptions::forced(), + ) + .await?; + + let status = { + let guard = state.rtsp.read().await; + if let Some(ref service) = *guard { + service.status().await + } else { + crate::rtsp::RtspServiceStatus::Stopped + } + }; + + Ok(Json(RtspStatusResponse::new(¤t_config, status))) +} + +pub async fn stop_rtsp_service( + State(state): State>, +) -> Result> { + let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?; + let current_config = state.config.get().rtsp.clone(); + let mut stop_config = current_config.clone(); + stop_config.enabled = false; + + apply_rtsp_config( + &state, + ¤t_config, + &stop_config, + ConfigApplyOptions::forced(), + ) + .await?; + + let status = { + let guard = state.rtsp.read().await; + if let Some(ref service) = *guard { + service.status().await + } else { + crate::rtsp::RtspServiceStatus::Stopped + } + }; + + Ok(Json(RtspStatusResponse::new(¤t_config, status))) +} diff --git a/src/web/handlers/config/rustdesk.rs b/src/web/handlers/config/rustdesk.rs index 3546c30b..9f99ed9e 100644 --- a/src/web/handlers/config/rustdesk.rs +++ b/src/web/handlers/config/rustdesk.rs @@ -16,7 +16,7 @@ pub struct RustDeskConfigResponse { pub device_id: String, pub has_password: bool, pub has_keypair: bool, - pub has_relay_key: bool, + pub relay_key: Option, } impl From<&RustDeskConfig> for RustDeskConfigResponse { @@ -28,7 +28,7 @@ impl From<&RustDeskConfig> for RustDeskConfigResponse { device_id: config.device_id.clone(), has_password: !config.device_password.is_empty(), has_keypair: config.public_key.is_some() && config.private_key.is_some(), - has_relay_key: config.relay_key.is_some(), + relay_key: config.relay_key.clone(), } } } @@ -144,3 +144,71 @@ pub async fn get_device_password(State(state): State>) -> Json>, +) -> Result> { + let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?; + let current_config = state.config.get().rustdesk.clone(); + let mut start_config = current_config.clone(); + start_config.enabled = true; + + apply_rustdesk_config( + &state, + ¤t_config, + &start_config, + ConfigApplyOptions::forced(), + ) + .await?; + + let (service_status, rendezvous_status) = { + let guard = state.rustdesk.read().await; + if let Some(ref service) = *guard { + let status = format!("{}", service.status()); + let rv_status = service.rendezvous_status().map(|s| format!("{}", s)); + (status, rv_status) + } else { + ("not_initialized".to_string(), None) + } + }; + + Ok(Json(RustDeskStatusResponse { + config: RustDeskConfigResponse::from(¤t_config), + service_status, + rendezvous_status, + })) +} + +pub async fn stop_rustdesk_service( + State(state): State>, +) -> Result> { + let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?; + let current_config = state.config.get().rustdesk.clone(); + let mut stop_config = current_config.clone(); + stop_config.enabled = false; + + apply_rustdesk_config( + &state, + ¤t_config, + &stop_config, + ConfigApplyOptions::forced(), + ) + .await?; + + let (service_status, rendezvous_status) = { + let guard = state.rustdesk.read().await; + if let Some(ref service) = *guard { + let status = format!("{}", service.status()); + let rv_status = service.rendezvous_status().map(|s| format!("{}", s)); + (status, rv_status) + } else { + ("not_initialized".to_string(), None) + } + }; + + Ok(Json(RustDeskStatusResponse { + config: RustDeskConfigResponse::from(¤t_config), + service_status, + rendezvous_status, + })) +} diff --git a/src/web/handlers/config/types.rs b/src/web/handlers/config/types.rs index f9c5935a..da04b0ab 100644 --- a/src/web/handlers/config/types.rs +++ b/src/web/handlers/config/types.rs @@ -90,7 +90,7 @@ impl VideoConfigUpdate { } } -/// Stream configuration response (includes has_turn_password) +/// Stream configuration response #[typeshare] #[derive(Debug, serde::Serialize)] pub struct StreamConfigResponse { @@ -104,8 +104,7 @@ pub struct StreamConfigResponse { pub stun_server: Option, pub turn_server: Option, pub turn_username: Option, - /// Indicates whether TURN password has been configured (password is not returned) - pub has_turn_password: bool, + pub turn_password: Option, } impl From<&StreamConfig> for StreamConfigResponse { @@ -120,7 +119,7 @@ impl From<&StreamConfig> for StreamConfigResponse { stun_server: config.stun_server.clone(), turn_server: config.turn_server.clone(), turn_username: config.turn_username.clone(), - has_turn_password: config.turn_password.is_some(), + turn_password: config.turn_password.clone(), } } } @@ -798,7 +797,7 @@ pub struct RtspConfigResponse { pub allow_one_client: bool, pub codec: RtspCodec, pub username: Option, - pub has_password: bool, + pub password: Option, } impl From<&RtspConfig> for RtspConfigResponse { @@ -811,7 +810,7 @@ impl From<&RtspConfig> for RtspConfigResponse { allow_one_client: config.allow_one_client, codec: config.codec.clone(), username: config.username.clone(), - has_password: config.password.is_some(), + password: config.password.clone(), } } } diff --git a/src/web/routes.rs b/src/web/routes.rs index 80aef025..ed43e80b 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -131,6 +131,14 @@ pub fn create_router(state: Arc) -> Router { "/config/rustdesk/regenerate-password", post(handlers::config::regenerate_device_password), ) + .route( + "/config/rustdesk/start", + post(handlers::config::start_rustdesk_service), + ) + .route( + "/config/rustdesk/stop", + post(handlers::config::stop_rustdesk_service), + ) // RTSP configuration endpoints .route("/config/rtsp", get(handlers::config::get_rtsp_config)) .route("/config/rtsp", patch(handlers::config::update_rtsp_config)) @@ -138,6 +146,14 @@ pub fn create_router(state: Arc) -> Router { "/config/rtsp/status", get(handlers::config::get_rtsp_status), ) + .route( + "/config/rtsp/start", + post(handlers::config::start_rtsp_service), + ) + .route( + "/config/rtsp/stop", + post(handlers::config::stop_rtsp_service), + ) // Web server configuration .route("/config/web", get(handlers::config::get_web_config)) .route("/config/web", patch(handlers::config::update_web_config)) diff --git a/web/src/api/config.ts b/web/src/api/config.ts index dcdbcf2f..3a9eb834 100644 --- a/web/src/api/config.ts +++ b/web/src/api/config.ts @@ -1,10 +1,3 @@ -/** - * 配置管理 API - 域分离架构 - * - * 每个配置域(video, stream, hid, msd, atx, audio)有独立的 GET/PATCH 端点, - * 避免配置项之间的相互干扰。 - */ - import type { AppConfig, AuthConfig, @@ -38,22 +31,12 @@ import type { import { request } from './request' export const configApi = { - /** - * 获取完整配置 - */ getAll: () => request('/config'), } export const authConfigApi = { - /** - * 获取认证配置 - */ get: () => request('/config/auth'), - /** - * 更新认证配置 - * @param config 要更新的字段 - */ update: (config: AuthConfigUpdate) => request('/config/auth', { method: 'PATCH', @@ -62,15 +45,8 @@ export const authConfigApi = { } export const videoConfigApi = { - /** - * 获取视频配置 - */ get: () => request('/config/video'), - /** - * 更新视频配置 - * @param config 要更新的字段(仅发送需要修改的字段) - */ update: (config: VideoConfigUpdate) => request('/config/video', { method: 'PATCH', @@ -79,15 +55,8 @@ export const videoConfigApi = { } export const streamConfigApi = { - /** - * 获取流配置 - */ get: () => request('/config/stream'), - /** - * 更新流配置 - * @param config 要更新的字段 - */ update: (config: StreamConfigUpdate) => request('/config/stream', { method: 'PATCH', @@ -96,15 +65,8 @@ export const streamConfigApi = { } export const hidConfigApi = { - /** - * 获取 HID 配置 - */ get: () => request('/config/hid'), - /** - * 更新 HID 配置 - * @param config 要更新的字段 - */ update: (config: HidConfigUpdate) => request('/config/hid', { method: 'PATCH', @@ -113,15 +75,8 @@ export const hidConfigApi = { } export const msdConfigApi = { - /** - * 获取 MSD 配置 - */ get: () => request('/config/msd'), - /** - * 更新 MSD 配置 - * @param config 要更新的字段 - */ update: (config: MsdConfigUpdate) => request('/config/msd', { method: 'PATCH', @@ -139,54 +94,29 @@ export interface WolHistoryResponse { } export const atxConfigApi = { - /** - * 获取 ATX 配置 - */ get: () => request('/config/atx'), - /** - * 更新 ATX 配置 - * @param config 要更新的字段 - */ update: (config: AtxConfigUpdate) => request('/config/atx', { method: 'PATCH', body: JSON.stringify(config), }), - /** - * 获取可用的 ATX 设备(GPIO chips, USB relays) - */ listDevices: () => request('/devices/atx'), - /** - * 发送 Wake-on-LAN 魔术包 - * @param macAddress 目标 MAC 地址 - */ sendWol: (macAddress: string) => request<{ success: boolean; message?: string }>('/atx/wol', { method: 'POST', body: JSON.stringify({ mac_address: macAddress }), }), - /** - * 获取 WOL 历史记录(服务端持久化) - * @param limit 返回条数(1-50) - */ getWolHistory: (limit = 5) => request(`/atx/wol/history?limit=${Math.max(1, Math.min(50, limit))}`), } export const audioConfigApi = { - /** - * 获取音频配置 - */ get: () => request('/config/audio'), - /** - * 更新音频配置 - * @param config 要更新的字段 - */ update: (config: AudioConfigUpdate) => request('/config/audio', { method: 'PATCH', @@ -195,59 +125,35 @@ export const audioConfigApi = { } export const extensionsApi = { - /** - * 获取所有扩展状态 - */ getAll: () => request('/extensions'), - /** - * 获取单个扩展状态 - */ get: (id: string) => request(`/extensions/${id}`), - /** - * 启动扩展 - */ start: (id: string) => request(`/extensions/${id}/start`, { method: 'POST', }), - /** - * 停止扩展 - */ stop: (id: string) => request(`/extensions/${id}/stop`, { method: 'POST', }), - /** - * 获取扩展日志 - */ logs: (id: string, lines = 100) => request(`/extensions/${id}/logs?lines=${lines}`), - /** - * 更新 ttyd 配置 - */ updateTtyd: (config: TtydConfigUpdate) => request('/extensions/ttyd/config', { method: 'PATCH', body: JSON.stringify(config), }), - /** - * 更新 gostc 配置 - */ updateGostc: (config: GostcConfigUpdate) => request('/extensions/gostc/config', { method: 'PATCH', body: JSON.stringify(config), }), - /** - * 更新 easytier 配置 - */ updateEasytier: (config: EasytierConfigUpdate) => request('/extensions/easytier/config', { method: 'PATCH', @@ -255,7 +161,6 @@ export const extensionsApi = { }), } -/** RustDesk 配置响应 */ export interface RustDeskConfigResponse { enabled: boolean rendezvous_server: string @@ -263,17 +168,15 @@ export interface RustDeskConfigResponse { device_id: string has_password: boolean has_keypair: boolean - has_relay_key: boolean + relay_key: string | null } -/** RustDesk 状态响应 */ export interface RustDeskStatusResponse { config: RustDeskConfigResponse service_status: string rendezvous_status: string | null } -/** RustDesk 配置更新 */ export interface RustDeskConfigUpdate { enabled?: boolean rendezvous_server?: string @@ -282,52 +185,37 @@ export interface RustDeskConfigUpdate { device_password?: string } -/** RustDesk 密码响应 */ export interface RustDeskPasswordResponse { device_id: string device_password: string } export const rustdeskConfigApi = { - /** - * 获取 RustDesk 配置 - */ get: () => request('/config/rustdesk'), - /** - * 更新 RustDesk 配置 - */ update: (config: RustDeskConfigUpdate) => request('/config/rustdesk', { method: 'PATCH', body: JSON.stringify(config), }), - /** - * 获取 RustDesk 完整状态 - */ getStatus: () => request('/config/rustdesk/status'), - /** - * 获取设备密码(管理员专用) - */ getPassword: () => request('/config/rustdesk/password'), - /** - * 重新生成设备 ID - */ regenerateId: () => request('/config/rustdesk/regenerate-id', { method: 'POST', }), - /** - * 重新生成设备密码 - */ regeneratePassword: () => request('/config/rustdesk/regenerate-password', { method: 'POST', }), + + start: () => request('/config/rustdesk/start', { method: 'POST' }), + + stop: () => request('/config/rustdesk/stop', { method: 'POST' }), } export type RtspCodec = 'h264' | 'h265' @@ -340,7 +228,7 @@ export interface RtspConfigResponse { allow_one_client: boolean codec: RtspCodec username?: string | null - has_password: boolean + password: string | null } export interface RtspConfigUpdate { @@ -369,22 +257,19 @@ export const rtspConfigApi = { }), getStatus: () => request('/config/rtsp/status'), + + start: () => request('/config/rtsp/start', { method: 'POST' }), + + stop: () => request('/config/rtsp/stop', { method: 'POST' }), } -/** REST `/config/web` 响应(`WebConfigResponse` 别名,兼容旧命名) */ export type WebConfig = WebConfigResponse export type { WebConfigUpdate } export const webConfigApi = { - /** - * 获取 Web 服务器配置 - */ get: () => request('/config/web'), - /** - * 更新 Web 服务器配置(含可选的证书上传) - */ update: (config: WebConfigUpdate) => request('/config/web', { method: 'PATCH', @@ -411,9 +296,6 @@ export const redfishConfigApi = { } export const systemApi = { - /** - * 重启系统 - */ restart: () => request<{ success: boolean; message?: string }>('/system/restart', { method: 'POST', diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index d1a7cfe2..a2342dee 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -873,8 +873,7 @@ export default { turnServerHint: 'TURN relay server. Strongly recommended for public deployments or strict NAT environments.', turnUsername: 'TURN Username', turnPassword: 'TURN Password', - turnPasswordConfigured: 'A password is already saved. Leave empty to keep the current password.', - turnCredentialsHint: 'Credentials used for TURN server authentication', + turnCredentialsHint: 'Credentials used for TURN server authentication', iceConfigNote: 'Changes apply to the next WebRTC session', }, virtualKeyboard: { @@ -994,15 +993,10 @@ export default { serverSettings: 'Server Settings', rendezvousServer: 'ID Server', rendezvousServerPlaceholder: 'hbbs.example.com:21116', - rendezvousServerHint: 'Configure your RustDesk server address (port optional, defaults to 21116)', rendezvousServerRequired: 'Enter the RustDesk ID server', relayServer: 'Relay Server', relayServerPlaceholder: 'hbbr.example.com:21117', - relayServerHint: 'Relay server address (port optional, defaults to 21117). Auto-derived if empty', relayKey: 'Relay Key', - relayKeyPlaceholder: 'e.g. pLU0pEj2IZnNVKzrIO1pIdwGA3dOVJJLkFIYGOCGH1E=', - relayKeySet: 'Saved (32-byte Base64, usually 44 chars; leave empty and save to keep)', - relayKeyHint: 'Same as hbbs/hbbr -k: standard Base64 decoding to exactly 32 bytes (typically 44 characters including trailing =)', deviceInfo: 'Device Info', deviceId: 'Device ID', deviceIdHint: 'Use this ID in RustDesk client to connect', @@ -1042,8 +1036,6 @@ export default { usernamePlaceholder: 'Empty means no authentication', password: 'Password', passwordPlaceholder: 'Enter new password', - passwordSet: '••••••••', - passwordHint: 'Leave empty to keep current password; enter a new value to update it.', urlPreview: 'RTSP URL Preview', }, }, diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index 69d7dbbb..61ae4353 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -719,7 +719,7 @@ export default { autoRecommended: '自动(推荐)', software: '软件', supportedFormats: '支持的编码格式', - encoderHint: '硬件编码器延迟低、CPU 占用小;软件编码器兼容性更广但占用资源更多。', + encoderHint: '硬件编码器延迟和 CPU 占用比软件编码低,画质预设更好', hidSettings: 'HID 设置', hidSettingsDesc: '配置键盘和鼠标控制', hidBackend: 'HID 后端', @@ -869,11 +869,10 @@ export default { stunServerHint: '留空将使用 Google 公共 STUN 服务器', turnServer: 'TURN 服务器', turnServerPlaceholder: 'turn:turn.example.com:3478', - turnServerHint: 'TURN 中继服务器;公网部署或严格 NAT 环境强烈建议配置', + turnServerHint: 'P2P 连接失败时进行流量中继', turnUsername: 'TURN 用户名', turnPassword: 'TURN 密码', - turnPasswordConfigured: '密码已保存。留空则保持当前密码不变。', - turnCredentialsHint: '用于 TURN 服务器身份验证的凭据', + turnCredentialsHint: '用于 TURN 服务器身份验证的凭据', iceConfigNote: '更改后将在下一次 WebRTC 会话建立时生效', }, virtualKeyboard: { @@ -993,15 +992,10 @@ export default { serverSettings: '服务器设置', rendezvousServer: 'ID 服务器', rendezvousServerPlaceholder: 'hbbs.example.com:21116', - rendezvousServerHint: '请配置您的 RustDesk 服务器地址(端口可省略,默认 21116)', rendezvousServerRequired: '请填写 RustDesk ID 服务器', relayServer: '中继服务器', relayServerPlaceholder: 'hbbr.example.com:21117', - relayServerHint: '中继服务器地址(端口可省略,默认 21117),留空则自动从 ID 服务器推导', relayKey: '中继密钥', - relayKeyPlaceholder: '例如 pLU0pEj2IZnNVKzrIO1pIdwGA3dOVJJLkFIYGOCGH1E=', - relayKeySet: '已保存(32 字节 Base64,通常 44 字符;留空保存则保留)', - relayKeyHint: '与 hbbs/hbbr 的 -k 一致:标准 Base64,解码后固定 32 字节(一般为 44 个字符,含末尾 =)', deviceInfo: '设备信息', deviceId: '设备 ID', deviceIdHint: '此 ID 用于 RustDesk 客户端连接', @@ -1041,8 +1035,6 @@ export default { usernamePlaceholder: '留空表示无需认证', password: '密码', passwordPlaceholder: '输入新密码', - passwordSet: '••••••••', - passwordHint: '留空表示保持当前密码;如需修改请输入新密码。', urlPreview: 'RTSP 地址预览', }, }, diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index b8897a18..133c8339 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -2,85 +2,51 @@ Generated by typeshare 1.13.4 */ -/** Authentication configuration */ export interface AuthConfig { - /** Session timeout in seconds */ session_timeout_secs: number; - /** Allow multiple concurrent web sessions (single-user mode) */ single_user_allow_multiple_sessions: boolean; - /** Enable 2FA */ totp_enabled: boolean; - /** TOTP secret (encrypted) */ totp_secret?: string; } -/** Video capture configuration */ export interface VideoConfig { - /** Video device path (e.g., /dev/video0) */ device?: string; - /** Video pixel format (e.g., "MJPEG", "YUYV", "NV12") */ format?: string; - /** Resolution width */ width: number; - /** Resolution height */ height: number; - /** Frame rate */ fps: number; - /** JPEG quality (1-100) */ quality: number; } -/** HID backend type */ export enum HidBackend { - /** USB OTG HID gadget */ Otg = "otg", - /** CH9329 serial HID controller */ Ch9329 = "ch9329", - /** Disabled */ None = "none", } -/** OTG USB device descriptor configuration */ export interface OtgDescriptorConfig { - /** USB Vendor ID (e.g., 0x1d6b) */ vendor_id: number; - /** USB Product ID (e.g., 0x0104) */ product_id: number; - /** Manufacturer string */ manufacturer: string; - /** Product string */ product: string; - /** Serial number (optional, auto-generated if not set) */ serial_number?: string; } -/** OTG HID function profile */ export enum OtgHidProfile { - /** Full HID device set (keyboard + relative mouse + absolute mouse + consumer control) */ Full = "full", - /** Full HID device set without consumer control */ FullNoConsumer = "full_no_consumer", - /** Legacy profile: only keyboard */ LegacyKeyboard = "legacy_keyboard", - /** Legacy profile: only relative mouse */ LegacyMouseRelative = "legacy_mouse_relative", - /** Custom function selection */ Custom = "custom", } -/** OTG endpoint budget policy. */ export enum OtgEndpointBudget { - /** Derive a safe default from the selected UDC. */ Auto = "auto", - /** Limit OTG gadget functions to 5 endpoints. */ Five = "five", - /** Limit OTG gadget functions to 6 endpoints. */ Six = "six", - /** Do not impose a software endpoint budget. */ Unlimited = "unlimited", } -/** OTG HID function selection (used when profile is Custom) */ export interface OtgHidFunctions { keyboard: boolean; mouse_relative: boolean; @@ -88,226 +54,104 @@ export interface OtgHidFunctions { consumer: boolean; } -/** HID configuration */ export interface HidConfig { - /** HID backend type */ backend: HidBackend; - /** OTG UDC (USB Device Controller) name */ otg_udc?: string; - /** OTG USB device descriptor configuration */ otg_descriptor?: OtgDescriptorConfig; - /** OTG HID function profile */ otg_profile?: OtgHidProfile; - /** OTG endpoint budget policy */ otg_endpoint_budget?: OtgEndpointBudget; - /** OTG HID function selection (used when profile is Custom) */ otg_functions?: OtgHidFunctions; - /** Enable keyboard LED/status feedback for OTG keyboard */ otg_keyboard_leds?: boolean; - /** CH9329 serial port */ ch9329_port: string; - /** CH9329 baud rate */ ch9329_baudrate: number; - /** Mouse mode: absolute or relative */ mouse_absolute: boolean; } -/** MSD configuration */ export interface MsdConfig { - /** Enable MSD functionality */ enabled: boolean; - /** MSD base directory (absolute path) */ msd_dir: string; } -/** Driver type for ATX key operations */ export enum AtxDriverType { - /** GPIO control via Linux character device */ Gpio = "gpio", - /** USB HID relay module */ UsbRelay = "usbrelay", - /** Serial/COM port relay (taobao LCUS type) */ Serial = "serial", - /** Disabled / Not configured */ None = "none", } -/** Active level for GPIO pins */ export enum ActiveLevel { - /** Active high (default for most cases) */ High = "high", - /** Active low (inverted) */ Low = "low", } -/** - * Configuration for a single ATX key (power or reset) - * This is the "four-tuple" configuration: (driver, device, pin/channel, level) - */ export interface AtxKeyConfig { - /** Driver type (GPIO or USB Relay) */ driver: AtxDriverType; - /** - * Device path: - * - For GPIO: /dev/gpiochipX - * - For USB Relay: /dev/hidrawX - */ device: string; - /** - * Pin or channel number: - * - For GPIO: GPIO pin number - * - For USB Relay: relay channel (0-based) - * - For Serial Relay (LCUS): relay channel (1-based) - */ pin: number; - /** Active level (only applicable to GPIO, ignored for USB Relay) */ active_level: ActiveLevel; - /** Baud rate for serial relay (start with 9600) */ baud_rate: number; } -/** LED sensing configuration (optional) */ export interface AtxLedConfig { - /** Whether LED sensing is enabled */ enabled: boolean; - /** GPIO chip for LED sensing */ gpio_chip: string; - /** GPIO pin for LED input */ gpio_pin: number; - /** Whether LED is active low (inverted logic) */ inverted: boolean; } -/** - * ATX power control configuration - * - * Each ATX action (power, reset) can be independently configured with its own - * hardware binding using the four-tuple: (driver, device, pin, active_level). - */ export interface AtxConfig { - /** Enable ATX functionality */ enabled: boolean; - /** Power button configuration (used for both short and long press) */ power: AtxKeyConfig; - /** Reset button configuration */ reset: AtxKeyConfig; - /** LED sensing configuration (optional) */ led: AtxLedConfig; - /** Network interface for WOL packets (empty = auto) */ wol_interface: string; } -/** - * Audio configuration - * - * Note: Sample rate is fixed at 48000Hz and channels at 2 (stereo). - * These are optimal for Opus encoding and match WebRTC requirements. - */ export interface AudioConfig { - /** Enable audio capture */ enabled: boolean; - /** ALSA device name */ device: string; - /** Audio quality preset: "voice", "balanced", "high" */ quality: string; } -/** Stream mode */ export enum StreamMode { - /** WebRTC with H264/H265 */ WebRTC = "webrtc", - /** MJPEG over HTTP */ Mjpeg = "mjpeg", } -/** Encoder type */ export enum EncoderType { - /** Auto-detect best encoder */ Auto = "auto", - /** Software encoder (libx264) */ Software = "software", - /** VAAPI hardware encoder */ Vaapi = "vaapi", - /** NVIDIA NVENC hardware encoder */ Nvenc = "nvenc", - /** Intel Quick Sync hardware encoder */ Qsv = "qsv", - /** AMD AMF hardware encoder */ Amf = "amf", - /** Rockchip MPP hardware encoder */ Rkmpp = "rkmpp", - /** V4L2 M2M hardware encoder */ V4l2m2m = "v4l2m2m", } -/** - * Bitrate preset for video encoding - * - * Simplifies bitrate configuration by providing three intuitive presets - * plus a custom option for advanced users. - */ export type BitratePreset = - /** - * Speed priority: 1 Mbps, lowest latency, smaller GOP - * Best for: slow networks, remote management, low-bandwidth scenarios - */ | { type: "Speed", value?: undefined } - /** - * Balanced: 4 Mbps, good quality/latency tradeoff - * Best for: typical usage, recommended default - */ | { type: "Balanced", value?: undefined } - /** - * Quality priority: 8 Mbps, best visual quality - * Best for: local network, high-bandwidth scenarios, detailed work - */ | { type: "Quality", value?: undefined } - /** Custom bitrate in kbps (for advanced users) */ | { type: "Custom", value: number }; -/** Streaming configuration */ export interface StreamConfig { - /** Stream mode */ mode: StreamMode; - /** Encoder type for H264/H265 */ encoder: EncoderType; - /** Bitrate preset (Speed/Balanced/Quality) */ bitrate_preset: BitratePreset; - /** - * Custom STUN server (e.g., "stun:stun.l.google.com:19302") - * If empty, uses public ICE servers from secrets.toml - */ stun_server?: string; - /** - * Custom TURN server (e.g., "turn:turn.example.com:3478") - * If empty, uses public ICE servers from secrets.toml - */ turn_server?: string; - /** TURN username */ turn_username?: string; - /** TURN password (stored encrypted in DB, not exposed via API) */ turn_password?: string; } -/** - * Web server configuration persisted in the database (includes on-disk TLS paths). - * - * The HTTP API for `/api/config/web` uses `WebConfigResponse` instead: no path fields, includes `has_custom_cert`. - */ export interface WebConfig { - /** HTTP port */ http_port: number; - /** HTTPS port */ https_port: number; - /** Bind addresses (preferred) */ bind_addresses: string[]; - /** Bind address (legacy) */ bind_address: string; - /** Enable HTTPS */ https_enabled: boolean; - /** Custom SSL certificate path */ ssl_cert_path?: string; - /** Custom SSL key path */ ssl_key_path?: string; } @@ -337,74 +181,46 @@ export interface ExtensionsConfig { easytier: EasytierConfig; } -/** RustDesk configuration */ export interface RustDeskConfig { - /** Enable RustDesk protocol */ enabled: boolean; - /** - * Rendezvous server address (hbbs), e.g., "rs.example.com" or "192.168.1.100:21116" - * Required for RustDesk to function - */ rendezvous_server: string; - /** - * Relay server address (hbbr), if different from rendezvous server - * Usually the same host as rendezvous server but different port (21117) - */ relay_server?: string; - /** Device ID (9-digit number), auto-generated if empty */ device_id: string; } -/** RTSP output codec */ export enum RtspCodec { H264 = "h264", H265 = "h265", } -/** RTSP configuration */ export interface RtspConfig { - /** Enable RTSP output */ enabled: boolean; - /** Bind IP address */ bind: string; - /** RTSP TCP listen port */ port: number; - /** Stream path (without leading slash) */ path: string; - /** Allow only one client connection at a time */ allow_one_client: boolean; - /** Output codec (H264/H265) */ codec: RtspCodec; - /** Optional username for authentication */ username?: string; } -/** Main application configuration */ +export interface RedfishConfig { + enabled: boolean; +} + export interface AppConfig { - /** Whether initial setup has been completed */ initialized: boolean; - /** Authentication settings */ auth: AuthConfig; - /** Video capture settings */ video: VideoConfig; - /** HID (keyboard/mouse) settings */ hid: HidConfig; - /** Mass Storage Device settings */ msd: MsdConfig; - /** ATX power control settings */ atx: AtxConfig; - /** Audio settings */ audio: AudioConfig; - /** Streaming settings */ stream: StreamConfig; - /** Web server settings */ web: WebConfig; - /** Extensions settings (ttyd, gostc, easytier) */ extensions: ExtensionsConfig; - /** RustDesk remote access settings */ rustdesk: RustDeskConfig; - /** RTSP streaming settings */ rtsp: RtspConfig; + redfish: RedfishConfig; } /** Update for a single ATX key configuration */ @@ -437,13 +253,9 @@ export interface AtxConfigUpdate { wol_interface?: string; } -/** Available ATX devices for discovery */ export interface AtxDevices { - /** Available GPIO chips (/dev/gpiochip*) */ gpio_chips: string[]; - /** Available USB HID relay devices (/dev/hidraw*) */ usb_relays: string[]; - /** Available Serial ports (/dev/ttyUSB*) */ serial_ports: string[]; } @@ -457,7 +269,6 @@ export interface AuthConfigUpdate { single_user_allow_multiple_sessions?: boolean; } -/** Update easytier config */ export interface EasytierConfigUpdate { enabled?: boolean; network_name?: string; @@ -471,9 +282,6 @@ export type ExtensionStatus = | { state: "stopped", data?: undefined } | { state: "running", data: { pid: number; -}} - | { state: "failed", data: { - error: string; }}; export interface EasytierInfo { @@ -516,7 +324,6 @@ export interface ExtensionsStatus { easytier: EasytierInfo; } -/** Update gostc config */ export interface GostcConfigUpdate { enabled?: boolean; addr?: string; @@ -566,7 +373,7 @@ export interface RtspConfigResponse { allow_one_client: boolean; codec: RtspCodec; username?: string; - has_password: boolean; + password?: string; } export interface RtspConfigUpdate { @@ -593,7 +400,7 @@ export interface RustDeskConfigUpdate { device_password?: string; } -/** Stream configuration response (includes has_turn_password) */ +/** Stream configuration response */ export interface StreamConfigResponse { mode: StreamMode; encoder: EncoderType; @@ -605,8 +412,7 @@ export interface StreamConfigResponse { stun_server?: string; turn_server?: string; turn_username?: string; - /** Indicates whether TURN password has been configured (password is not returned) */ - has_turn_password: boolean; + turn_password?: string; } export interface StreamConfigUpdate { @@ -629,7 +435,6 @@ export interface StreamConfigUpdate { turn_password?: string; } -/** Update ttyd config */ export interface TtydConfigUpdate { enabled?: boolean; shell?: string; @@ -673,12 +478,6 @@ export interface WebConfigUpdate { clear_custom_cert?: boolean; } -/** - * Shared canonical keyboard key identifiers used across frontend and backend. - * - * The enum names intentionally mirror `KeyboardEvent.code` style values so the - * browser, virtual keyboard, and HID backend can all speak the same language. - */ export enum CanonicalKey { KeyA = "KeyA", KeyB = "KeyB", diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index 71a8a540..ebae469c 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -14,6 +14,8 @@ import { atxConfigApi, extensionsApi, redfishConfigApi, + rtspConfigApi, + rustdeskConfigApi, systemApi, updateApi, usbApi, @@ -295,7 +297,6 @@ const passwordSaving = ref(false) const passwordSaved = ref(false) const passwordError = ref('') const showPasswords = ref(false) - const authConfig = ref({ session_timeout_secs: 3600 * 24, single_user_allow_multiple_sessions: false, @@ -526,8 +527,6 @@ const config = ref({ turn_password: '', }) -const hasTurnPassword = ref(false) - type OtgSelfCheckLevel = 'info' | 'warn' | 'error' type OtgCheckGroupStatus = 'ok' | 'warn' | 'error' | 'skipped' @@ -1206,16 +1205,12 @@ async function saveConfig() { try { if (activeSection.value === 'video') { - const turnUrl = config.value.turn_server.trim() await configStore.updateStream({ encoder: config.value.encoder_backend as any, stun_server: config.value.stun_server.trim(), - turn_server: turnUrl, + turn_server: config.value.turn_server.trim(), turn_username: config.value.turn_username.trim(), - turn_password: - turnUrl === '' - ? '' - : config.value.turn_password || undefined, + turn_password: config.value.turn_password.trim(), }) await configStore.updateVideo({ device: config.value.video_device || undefined, @@ -1303,11 +1298,9 @@ async function loadConfig() { stun_server: stream.stun_server || '', turn_server: stream.turn_server || '', turn_username: stream.turn_username || '', - turn_password: '', // Password is never returned from server; set-only field + turn_password: stream.turn_password || '', } - hasTurnPassword.value = stream.has_turn_password || false - if (hid.otg_descriptor) { otgVendorIdHex.value = hid.otg_descriptor.vendor_id?.toString(16).padStart(4, '0') || '1d6b' otgProductIdHex.value = hid.otg_descriptor.product_id?.toString(16).padStart(4, '0') || '0104' @@ -1448,7 +1441,6 @@ function getExtStatusText(status: ExtensionStatus | undefined): string { case 'unavailable': return t('extensions.unavailable') case 'stopped': return t('extensions.stopped') case 'running': return t('extensions.running') - case 'failed': return t('extensions.failed') default: return t('extensions.stopped') } } @@ -1459,7 +1451,6 @@ function getExtStatusClass(status: ExtensionStatus | undefined): string { case 'unavailable': return 'bg-gray-400' case 'stopped': return 'bg-gray-400' case 'running': return 'bg-green-500' - case 'failed': return 'bg-red-500' default: return 'bg-gray-400' } } @@ -1616,19 +1607,23 @@ watch( }, ) +function applyRustdeskStatus(status: RustDeskStatusResponse) { + const config = status.config + rustdeskConfig.value = config + rustdeskStatus.value = status + rustdeskLocalConfig.value = { + enabled: config.enabled, + rendezvous_server: config.rendezvous_server, + relay_server: config.relay_server || '', + relay_key: config.relay_key || '', + } +} + async function loadRustdeskConfig() { rustdeskLoading.value = true try { const status = await configStore.refreshRustdeskStatus() - const config = status.config - rustdeskConfig.value = config - rustdeskStatus.value = status - rustdeskLocalConfig.value = { - enabled: config.enabled, - rendezvous_server: config.rendezvous_server, - relay_server: config.relay_server || '', - relay_key: '', - } + applyRustdeskStatus(status) } catch { } finally { rustdeskLoading.value = false @@ -1642,17 +1637,17 @@ async function loadRustdeskPassword() { } } -function normalizeRustdeskServer(value: string, defaultPort: number): string | undefined { +function normalizeRustdeskServer(value: string, defaultPort: number): string { const trimmed = value.trim() - if (!trimmed) return undefined + if (!trimmed) return '' if (trimmed.includes(':')) return trimmed return `${trimmed}:${defaultPort}` } -/** Strip line breaks from pasted keys; empty means “do not change” on PATCH. */ -function normalizeRustdeskRelayKey(value: string): string | undefined { +/** Strip line breaks from pasted keys. */ +function normalizeRustdeskRelayKey(value: string): string { const cleaned = value.replace(/\r?\n/g, '').trim() - return cleaned || undefined + return cleaned } function showValidationError(message: string): boolean { @@ -2002,7 +1997,6 @@ async function saveRustdeskConfig() { relay_key: normalizeRustdeskRelayKey(rustdeskLocalConfig.value.relay_key), }) await loadRustdeskConfig() - rustdeskLocalConfig.value.relay_key = '' saved.value = true setTimeout(() => (saved.value = false), 2000) } catch { @@ -2042,9 +2036,8 @@ async function startRustdesk() { rustdeskLoading.value = true try { - await configStore.updateRustdesk({ enabled: true }) - rustdeskLocalConfig.value.enabled = true - await loadRustdeskConfig() + const status = await rustdeskConfigApi.start() + applyRustdeskStatus(status) } catch { } finally { rustdeskLoading.value = false @@ -2054,9 +2047,8 @@ async function startRustdesk() { async function stopRustdesk() { rustdeskLoading.value = true try { - await configStore.updateRustdesk({ enabled: false }) - rustdeskLocalConfig.value.enabled = false - await loadRustdeskConfig() + const status = await rustdeskConfigApi.stop() + applyRustdeskStatus(status) } catch { } finally { rustdeskLoading.value = false @@ -2113,21 +2105,25 @@ function getRustdeskStatusClass(status: string | null | undefined): string { } } +function applyRtspStatus(status: RtspStatusResponse) { + rtspStatus.value = status + rtspLocalConfig.value = { + enabled: status.config.enabled, + bind: status.config.bind, + port: status.config.port, + path: status.config.path, + allow_one_client: status.config.allow_one_client, + codec: status.config.codec, + username: status.config.username || '', + password: status.config.password || '', + } +} + async function loadRtspConfig() { rtspLoading.value = true try { const status = await configStore.refreshRtspStatus() - rtspStatus.value = status - rtspLocalConfig.value = { - enabled: status.config.enabled, - bind: status.config.bind, - port: status.config.port, - path: status.config.path, - allow_one_client: status.config.allow_one_client, - codec: status.config.codec, - username: status.config.username || '', - password: '', - } + applyRtspStatus(status) } catch { } finally { rtspLoading.value = false @@ -2148,14 +2144,10 @@ async function saveRtspConfig() { username: (rtspLocalConfig.value.username || '').trim(), } - const nextPassword = (rtspLocalConfig.value.password || '').trim() - if (nextPassword) { - update.password = nextPassword - } + update.password = (rtspLocalConfig.value.password || '').trim() await configStore.updateRtsp(update) await loadRtspConfig() - rtspLocalConfig.value.password = '' saved.value = true setTimeout(() => (saved.value = false), 2000) } catch { @@ -2167,9 +2159,8 @@ async function saveRtspConfig() { async function startRtsp() { rtspLoading.value = true try { - await configStore.updateRtsp({ enabled: true }) - rtspLocalConfig.value.enabled = true - await loadRtspConfig() + const status = await rtspConfigApi.start() + applyRtspStatus(status) } catch { } finally { rtspLoading.value = false @@ -2179,9 +2170,8 @@ async function startRtsp() { async function stopRtsp() { rtspLoading.value = true try { - await configStore.updateRtsp({ enabled: false }) - rtspLocalConfig.value.enabled = false - await loadRtspConfig() + const status = await rtspConfigApi.stop() + applyRtspStatus(status) } catch { } finally { rtspLoading.value = false @@ -2573,7 +2563,7 @@ watch(isWindows, () => {
@@ -2583,8 +2573,7 @@ watch(isWindows, () => { id="turn-password" v-model="config.turn_password" :type="showPasswords ? 'text' : 'password'" - :disabled="!config.turn_server" - :placeholder="hasTurnPassword ? '••••••••' : ''" + :disabled="!config.stun_server && !config.turn_server" />
-

{{ t('settings.turnPasswordConfigured') }}

{{ t('settings.turnCredentialsHint') }}

@@ -4007,10 +3995,10 @@ watch(isWindows, () => {
+
@@ -4089,7 +4088,7 @@ watch(isWindows, () => {
-
@@ -4154,7 +4153,7 @@ watch(isWindows, () => {
- +
@@ -4162,8 +4161,8 @@ watch(isWindows, () => { -

{{ t('extensions.rustdesk.rendezvousServerHint') }}

{{ rustdeskValidationMessage }}

@@ -4173,23 +4172,33 @@ watch(isWindows, () => { -

{{ t('extensions.rustdesk.relayServerHint') }}

- -

{{ t('extensions.rustdesk.relayKeyHint') }}

+
+ + +
@@ -4259,7 +4268,7 @@ watch(isWindows, () => {
-