From e337b6d2de7cbeb43a168297f9761ecf1f050646 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 1 Mar 2026 18:12:31 +0100 Subject: [PATCH] [feat](trx-client): link web header title to website Add an optional website URL to config and use it for the web header title when present, falling back to the version title otherwise. Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- src/trx-client/src/config.rs | 12 +++++++++++ src/trx-client/src/main.rs | 1 + src/trx-client/trx-frontend/src/lib.rs | 3 +++ .../trx-frontend-http/assets/web/app.js | 21 ++++++++++++++++++- .../trx-frontend-http/assets/web/style.css | 8 +++++++ .../trx-frontend/trx-frontend-http/src/api.rs | 9 ++++++++ 6 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/trx-client/src/config.rs b/src/trx-client/src/config.rs index 10fe21e..3d08496 100644 --- a/src/trx-client/src/config.rs +++ b/src/trx-client/src/config.rs @@ -37,6 +37,8 @@ pub struct ClientConfig { pub struct GeneralConfig { /// Callsign or owner label to display in frontends pub callsign: Option, + /// Optional website URL to use as the web UI header title link. + pub website_url: Option, /// Log level (trace, debug, info, warn, error) pub log_level: Option, } @@ -45,6 +47,7 @@ impl Default for GeneralConfig { fn default() -> Self { Self { callsign: Some("N0CALL".to_string()), + website_url: None, log_level: None, } } @@ -334,6 +337,11 @@ impl ClientConfig { return Err("[remote.auth].token must not be empty when set".to_string()); } } + if let Some(url) = &self.general.website_url { + if url.trim().is_empty() { + return Err("[general].website_url must not be empty when set".to_string()); + } + } if self.frontends.http.enabled && self.frontends.http.port == 0 { return Err("[frontends.http].port must be > 0 when enabled".to_string()); @@ -424,6 +432,7 @@ impl ClientConfig { let example = ClientConfig { general: GeneralConfig { callsign: Some("N0CALL".to_string()), + website_url: Some("https://haxx.space".to_string()), log_level: Some("info".to_string()), }, remote: RemoteConfig { @@ -545,6 +554,7 @@ mod tests { assert!(config.frontends.http_json.enabled); assert_eq!(config.frontends.http_json.port, 0); assert!(config.remote.url.is_none()); + assert!(config.general.website_url.is_none()); assert_eq!(config.remote.poll_interval_ms, 750); assert!(config.frontends.audio.enabled); assert_eq!(config.frontends.audio.server_port, 4531); @@ -559,6 +569,7 @@ mod tests { let toml_str = r#" [general] callsign = "W1AW" +website_url = "https://example.com" [remote] url = "192.168.1.100:9000" @@ -576,6 +587,7 @@ initial_map_zoom = 12 let config: ClientConfig = toml::from_str(toml_str).unwrap(); assert_eq!(config.general.callsign, Some("W1AW".to_string())); + assert_eq!(config.general.website_url, Some("https://example.com".to_string())); assert_eq!(config.remote.url, Some("192.168.1.100:9000".to_string())); assert_eq!(config.remote.rig_id, Some("hf".to_string())); assert_eq!(config.remote.auth.token, Some("my-token".to_string())); diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index 742c70a..6e2d8dc 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -244,6 +244,7 @@ async fn async_init() -> DynResult { .clone() .or_else(|| cfg.general.callsign.clone()); frontend_runtime.owner_callsign = callsign.clone(); + frontend_runtime.owner_website_url = cfg.general.website_url.clone(); info!( "Starting trx-client (remote: {}, frontends: {})", diff --git a/src/trx-client/trx-frontend/src/lib.rs b/src/trx-client/trx-frontend/src/lib.rs index 95cb304..15986d3 100644 --- a/src/trx-client/trx-frontend/src/lib.rs +++ b/src/trx-client/trx-frontend/src/lib.rs @@ -176,6 +176,8 @@ pub struct FrontendRuntimeContext { pub remote_rigs: Arc>>, /// Owner callsign from trx-client config/CLI for frontend display. pub owner_callsign: Option, + /// Optional website URL for the web UI header title link. + pub owner_website_url: Option, /// Latest spectrum frame from the active SDR rig; None for non-SDR backends. pub spectrum: Arc>, } @@ -208,6 +210,7 @@ impl FrontendRuntimeContext { remote_active_rig_id: Arc::new(Mutex::new(None)), remote_rigs: Arc::new(Mutex::new(Vec::new())), owner_callsign: None, + owner_website_url: None, spectrum: Arc::new(Mutex::new(SharedSpectrum::default())), } } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 2af7c4a..16d2667 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -1265,6 +1265,7 @@ let serverVersion = null; let serverBuildDate = null; let serverCallsign = null; let ownerCallsign = null; +let ownerWebsiteUrl = null; let serverRigs = []; let serverActiveRigId = null; let serverLat = null; @@ -1282,11 +1283,26 @@ function updateFooterBuildInfo() { function updateTitle() { const titleEl = document.getElementById("rig-title"); if (titleEl) { - titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs"; + if (ownerWebsiteUrl) { + const label = ownerCallsign || displayLabelFromUrl(ownerWebsiteUrl); + titleEl.innerHTML = + `${escapeMapHtml(label)}`; + } else { + titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs"; + } } updateDocumentTitle(lastSpectrumData?.rds ?? null); } +function displayLabelFromUrl(url) { + try { + const host = new URL(url).hostname.replace(/^www\./i, ""); + return host || url; + } catch (_e) { + return url; + } +} + function render(update) { if (!update) return; if (update.server_version) serverVersion = update.server_version; @@ -1295,6 +1311,9 @@ function render(update) { if (typeof update.owner_callsign === "string" && update.owner_callsign.length > 0) { ownerCallsign = update.owner_callsign; } + if (typeof update.owner_website_url === "string" && update.owner_website_url.length > 0) { + ownerWebsiteUrl = update.owner_website_url; + } if (update.server_latitude != null) serverLat = update.server_latitude; if (update.server_longitude != null) serverLon = update.server_longitude; if (typeof update.initial_map_zoom === "number" && Number.isFinite(update.initial_map_zoom)) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 0eae7c0..65d3b13 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -508,6 +508,14 @@ small { color: var(--text-muted); } flex: 0 1 auto; } .title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; } +.title-link { + color: inherit; + text-decoration: none; +} +.title-link:hover { + text-decoration: underline; + text-underline-offset: 0.14em; +} .overview-strip { width: 100%; margin: 0; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 3aad0c5..cddffd7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -36,6 +36,7 @@ struct FrontendMeta { active_rig_id: Option, rig_ids: Vec, owner_callsign: Option, + owner_website_url: Option, show_sdr_gain_control: bool, initial_map_zoom: u8, } @@ -82,6 +83,9 @@ fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String { if let Some(owner) = meta.owner_callsign { map.insert("owner_callsign".to_string(), serde_json::json!(owner)); } + if let Some(url) = meta.owner_website_url { + map.insert("owner_website_url".to_string(), serde_json::json!(url)); + } map.insert( "show_sdr_gain_control".to_string(), serde_json::json!(meta.show_sdr_gain_control), @@ -105,6 +109,7 @@ fn frontend_meta_from_context( active_rig_id: active_rig_id_from_context(context), rig_ids: rig_ids_from_context(context), owner_callsign: owner_callsign_from_context(context), + owner_website_url: owner_website_url_from_context(context), show_sdr_gain_control: show_sdr_gain_control_from_context(context), initial_map_zoom: initial_map_zoom_from_context(context), } @@ -140,6 +145,10 @@ fn owner_callsign_from_context(context: &FrontendRuntimeContext) -> Option Option { + context.owner_website_url.clone() +} + fn show_sdr_gain_control_from_context(context: &FrontendRuntimeContext) -> bool { context.http_show_sdr_gain_control }