From 79e266b52d0826ec0a00f25ed181dcf7902f9c89 Mon Sep 17 00:00:00 2001 From: foxxcn Date: Sun, 11 Oct 2026 18:05:34 +0800 Subject: [PATCH] webui: fix settings that showed wrong or empty values - Keyboard layout: option values were en_US/de_DE/..., but the device reports and accepts us/uk/de/..., so the select was blank and any change was rejected. Use the device codes (and add Korean). - Power: the DC section read supported/enabled fields the device never sends, so it always said "not supported"; ATX state was requested even when ATX is not mounted, which logged an ERROR on every open and left the buttons live. Show DC or ATX controls by the active extension, read isOn/restoreState and the V/A/W reading, and hint when neither extension is active. - Mouse: switching to relative mode now checks that the relative mouse USB function is enabled and says how to enable it instead of silently sending reports the host never receives. - Network: show gateway and DNS, and note when the page was loaded via a different address than the one reported (the device has two NICs). - About: omit the empty system version. Co-Authored-By: Claude Opus 5.5 --- crates/rkvm-web/src/app.rs | 24 ++++++- crates/rkvm-web/src/settings.rs | 64 ++++++++++++++----- crates/rkvm-web/src/settings_advanced.rs | 80 +++++++++++++++++------- 3 files changed, 129 insertions(+), 39 deletions(-) diff --git a/crates/rkvm-web/src/app.rs b/crates/rkvm-web/src/app.rs index c42532c..c36dc31 100644 --- a/crates/rkvm-web/src/app.rs +++ b/crates/rkvm-web/src/app.rs @@ -185,8 +185,28 @@ pub fn App() -> impl IntoView { let toggle_mouse = move |_| { let next = !mouse_relative.get(); - mouse_relative.set(next); - mouse_mode.get_value().set(next); + if !next { + mouse_relative.set(false); + mouse_mode.get_value().set(false); + return; + } + // Relative reports go to a separate USB function; without it the host + // receives nothing, so only switch when the device has it enabled. + let Some(client) = rpc_store.get_value() else { return }; + spawn_local(async move { + match client.call("getUsbDevices", Value::Null).await { + Ok(v) if v.get("relative_mouse").and_then(Value::as_bool).unwrap_or(false) => { + error.set(None); + mouse_relative.set(true); + mouse_mode.get_value().set(true); + } + Ok(_) => error.set(Some( + "Relative mouse is disabled: enable it in ⚙ Settings → USB devices first" + .to_string(), + )), + Err(e) => error.set(Some(e.to_string())), + } + }); }; let do_reboot = move |_| { diff --git a/crates/rkvm-web/src/settings.rs b/crates/rkvm-web/src/settings.rs index 1149ff9..9ffff0e 100644 --- a/crates/rkvm-web/src/settings.rs +++ b/crates/rkvm-web/src/settings.rs @@ -13,14 +13,17 @@ use crate::net::{rest, upload}; use crate::rpc::RpcClient; use crate::util::{Rpc, as_string, fire, json_str}; +/// Values are the codes `getKeyboardLayout` returns and `setKeyboardLayout` +/// accepts on the device. const KEYBOARD_LAYOUTS: &[(&str, &str)] = &[ - ("en_US", "English (US)"), - ("en_UK", "English (UK)"), - ("de_DE", "German"), - ("fr_FR", "French"), - ("es_ES", "Spanish"), - ("it_IT", "Italian"), - ("ja_JP", "Japanese"), + ("us", "English (US)"), + ("uk", "English (UK)"), + ("de", "German"), + ("fr", "French"), + ("es", "Spanish"), + ("it", "Italian"), + ("jp", "Japanese"), + ("kr", "Korean"), ]; #[component] @@ -109,7 +112,11 @@ pub fn Settings( if let Ok(v) = client.call("getLocalVersion", Value::Null).await { let app = json_str(&v, "app_version"); let sys = json_str(&v, "system_version"); - version.set(format!("app {app} · system {sys}")); + version.set(if sys.is_empty() { + format!("app {app}") + } else { + format!("app {app} · system {sys}") + }); } }), Box::pin(async move { @@ -119,13 +126,7 @@ pub fn Settings( }), Box::pin(async move { if let Ok(v) = client.call("getNetworkState", Value::Null).await { - let ip = json_str(&v, "ip"); - let online = v.get("online").and_then(Value::as_bool).unwrap_or(false); - network.set(format!( - "{} ({})", - if ip.is_empty() { "—" } else { &ip }, - if online { "online" } else { "offline" } - )); + network.set(describe_network(&v)); } }), Box::pin(async move { @@ -262,7 +263,7 @@ pub fn Settings( spawn_local(async move { let _ = client.call("renewDHCPLease", Value::Null).await; if let Ok(v) = client.call("getNetworkState", Value::Null).await { - network.set(json_str(&v, "ip")); + network.set(describe_network(&v)); } }); }; @@ -779,6 +780,37 @@ impl UsbDevices { } } +/// One line for the Network section: address, link state, gateway and DNS. +/// The device can have several interfaces, so when this page was loaded from a +/// different address than the one reported, say so instead of looking wrong. +fn describe_network(v: &Value) -> String { + let ip = json_str(v, "ip"); + let online = v.get("online").and_then(Value::as_bool).unwrap_or(false); + let mut line = format!( + "{} ({})", + if ip.is_empty() { "—" } else { &ip }, + if online { "online" } else { "offline" } + ); + let gateway = json_str(v, "gateway"); + if !gateway.is_empty() { + line.push_str(&format!(" · gateway {gateway}")); + } + let dns: Vec<&str> = v + .get("dns") + .and_then(Value::as_array) + .map(|a| a.iter().filter_map(Value::as_str).collect()) + .unwrap_or_default(); + if !dns.is_empty() { + line.push_str(&format!(" · DNS {}", dns.join(", "))); + } + let page_host = + web_sys::window().and_then(|w| w.location().hostname().ok()).unwrap_or_default(); + if !ip.is_empty() && !page_host.is_empty() && page_host != ip { + line.push_str(&format!(" · this page via {page_host}")); + } + line +} + fn describe_mount(v: &Value) -> String { if v.is_null() { return "none".to_string(); diff --git a/crates/rkvm-web/src/settings_advanced.rs b/crates/rkvm-web/src/settings_advanced.rs index 299e1d0..9c5c22a 100644 --- a/crates/rkvm-web/src/settings_advanced.rs +++ b/crates/rkvm-web/src/settings_advanced.rs @@ -7,8 +7,11 @@ use crate::util::{Rpc, as_string, fire, json_str, json_string_list}; #[component] pub fn AdvancedSettings(open: RwSignal, rpc: Rpc) -> impl IntoView { let dc_enabled = RwSignal::new(false); - let dc_supported = RwSignal::new(false); + let dc_restore = RwSignal::new(String::new()); + let dc_reading = RwSignal::new(String::new()); let atx_state = RwSignal::new(String::new()); + // "dc-power", "atx-power" or "" (no power extension mounted). + let power_ext = RwSignal::new(String::new()); let cloud_state = RwSignal::new(String::new()); let cloud_api = RwSignal::new(String::new()); let cloud_app = RwSignal::new(String::new()); @@ -33,22 +36,52 @@ pub fn AdvancedSettings(open: RwSignal, rpc: Rpc) -> impl IntoView { } let Some(c) = rpc.get_value() else { return }; spawn_local(async move { - if let Ok(v) = c.call("getDCPowerState", Value::Null).await { - dc_enabled.set(v.get("enabled").and_then(Value::as_bool).unwrap_or(false)); - dc_supported.set(v.get("supported").and_then(Value::as_bool).unwrap_or(false)); - } - if let Ok(v) = c.call("getATXState", Value::Null).await { - atx_state.set(compact_json(&v)); + // Power controls depend on which extension is mounted. Asking for + // ATX state while it is not mounted only logs an error on the device. + let ext = match c.call("getActiveExtension", Value::Null).await { + Ok(v) => as_string(&v), + Err(_) => String::new(), + }; + match ext.as_str() { + "dc-power" => { + if let Ok(v) = c.call("getDCPowerState", Value::Null).await { + dc_enabled.set(v.get("isOn").and_then(Value::as_bool).unwrap_or(false)); + dc_restore.set( + v.get("restoreState") + .and_then(Value::as_i64) + .map(|s| s.to_string()) + .unwrap_or_default(), + ); + let num = |k: &str| v.get(k).and_then(Value::as_f64).unwrap_or(0.0); + dc_reading.set(format!( + "{:.2} V · {:.2} A · {:.2} W", + num("voltage"), + num("current"), + num("power") + )); + } + } + "atx-power" => match c.call("getATXState", Value::Null).await { + Ok(v) => { + let on = |k: &str| v.get(k).and_then(Value::as_bool).unwrap_or(false); + atx_state.set(format!( + "power LED {} · HDD LED {}", + if on("power") { "on" } else { "off" }, + if on("hdd") { "on" } else { "off" } + )); + } + Err(e) => atx_state.set(e.to_string()), + }, + _ => {} } + active_ext.set(ext.clone()); + power_ext.set(ext); if let Ok(v) = c.call("getCloudState", Value::Null).await { cloud_state.set(compact_json(&v)); } if let Ok(v) = c.call("getTailscaleControlURL", Value::Null).await { ts_url.set(as_string(&v)); } - if let Ok(v) = c.call("getActiveExtension", Value::Null).await { - active_ext.set(as_string(&v)); - } if let Ok(v) = c.call("getDevModeState", Value::Null).await { dev_mode.set(v.get("enabled").and_then(Value::as_bool).unwrap_or(false)); } @@ -173,26 +206,31 @@ pub fn AdvancedSettings(open: RwSignal, rpc: Rpc) -> impl IntoView { view! {

"Power"

- "DC power not supported" } - > + - -
"ATX: "{move || atx_state.get()}
-
- - - -
+ +
"ATX: "{move || atx_state.get()}
+
+ + + +
+
+ +
+ "No power extension active. Choose atx-power or dc-power under Extension to control host power." +
+