[feat](trx-client): support website_name in header

Add an optional website_name config field and prefer it over
callsign for the linked web header title label.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-01 18:19:36 +01:00
parent 31b40fc1ef
commit 00f5108a58
5 changed files with 30 additions and 1 deletions
+12
View File
@@ -39,6 +39,8 @@ pub struct GeneralConfig {
pub callsign: Option<String>, pub callsign: Option<String>,
/// Optional website URL to use as the web UI header title link. /// Optional website URL to use as the web UI header title link.
pub website_url: Option<String>, pub website_url: Option<String>,
/// Optional website name to use as the web UI header title label.
pub website_name: Option<String>,
/// Log level (trace, debug, info, warn, error) /// Log level (trace, debug, info, warn, error)
pub log_level: Option<String>, pub log_level: Option<String>,
} }
@@ -48,6 +50,7 @@ impl Default for GeneralConfig {
Self { Self {
callsign: Some("N0CALL".to_string()), callsign: Some("N0CALL".to_string()),
website_url: None, website_url: None,
website_name: None,
log_level: None, log_level: None,
} }
} }
@@ -342,6 +345,11 @@ impl ClientConfig {
return Err("[general].website_url must not be empty when set".to_string()); return Err("[general].website_url must not be empty when set".to_string());
} }
} }
if let Some(name) = &self.general.website_name {
if name.trim().is_empty() {
return Err("[general].website_name must not be empty when set".to_string());
}
}
if self.frontends.http.enabled && self.frontends.http.port == 0 { if self.frontends.http.enabled && self.frontends.http.port == 0 {
return Err("[frontends.http].port must be > 0 when enabled".to_string()); return Err("[frontends.http].port must be > 0 when enabled".to_string());
@@ -433,6 +441,7 @@ impl ClientConfig {
general: GeneralConfig { general: GeneralConfig {
callsign: Some("N0CALL".to_string()), callsign: Some("N0CALL".to_string()),
website_url: Some("https://haxx.space".to_string()), website_url: Some("https://haxx.space".to_string()),
website_name: Some("haxx.space".to_string()),
log_level: Some("info".to_string()), log_level: Some("info".to_string()),
}, },
remote: RemoteConfig { remote: RemoteConfig {
@@ -555,6 +564,7 @@ mod tests {
assert_eq!(config.frontends.http_json.port, 0); assert_eq!(config.frontends.http_json.port, 0);
assert!(config.remote.url.is_none()); assert!(config.remote.url.is_none());
assert!(config.general.website_url.is_none()); assert!(config.general.website_url.is_none());
assert!(config.general.website_name.is_none());
assert_eq!(config.remote.poll_interval_ms, 750); assert_eq!(config.remote.poll_interval_ms, 750);
assert!(config.frontends.audio.enabled); assert!(config.frontends.audio.enabled);
assert_eq!(config.frontends.audio.server_port, 4531); assert_eq!(config.frontends.audio.server_port, 4531);
@@ -570,6 +580,7 @@ mod tests {
[general] [general]
callsign = "W1AW" callsign = "W1AW"
website_url = "https://example.com" website_url = "https://example.com"
website_name = "Example"
[remote] [remote]
url = "192.168.1.100:9000" url = "192.168.1.100:9000"
@@ -588,6 +599,7 @@ initial_map_zoom = 12
let config: ClientConfig = toml::from_str(toml_str).unwrap(); let config: ClientConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.general.callsign, Some("W1AW".to_string())); assert_eq!(config.general.callsign, Some("W1AW".to_string()));
assert_eq!(config.general.website_url, Some("https://example.com".to_string())); assert_eq!(config.general.website_url, Some("https://example.com".to_string()));
assert_eq!(config.general.website_name, Some("Example".to_string()));
assert_eq!(config.remote.url, Some("192.168.1.100:9000".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.rig_id, Some("hf".to_string()));
assert_eq!(config.remote.auth.token, Some("my-token".to_string())); assert_eq!(config.remote.auth.token, Some("my-token".to_string()));
+1
View File
@@ -245,6 +245,7 @@ async fn async_init() -> DynResult<AppState> {
.or_else(|| cfg.general.callsign.clone()); .or_else(|| cfg.general.callsign.clone());
frontend_runtime.owner_callsign = callsign.clone(); frontend_runtime.owner_callsign = callsign.clone();
frontend_runtime.owner_website_url = cfg.general.website_url.clone(); frontend_runtime.owner_website_url = cfg.general.website_url.clone();
frontend_runtime.owner_website_name = cfg.general.website_name.clone();
info!( info!(
"Starting trx-client (remote: {}, frontends: {})", "Starting trx-client (remote: {}, frontends: {})",
+3
View File
@@ -178,6 +178,8 @@ pub struct FrontendRuntimeContext {
pub owner_callsign: Option<String>, pub owner_callsign: Option<String>,
/// Optional website URL for the web UI header title link. /// Optional website URL for the web UI header title link.
pub owner_website_url: Option<String>, pub owner_website_url: Option<String>,
/// Optional website name for the web UI header title label.
pub owner_website_name: Option<String>,
/// Latest spectrum frame from the active SDR rig; None for non-SDR backends. /// Latest spectrum frame from the active SDR rig; None for non-SDR backends.
pub spectrum: Arc<Mutex<SharedSpectrum>>, pub spectrum: Arc<Mutex<SharedSpectrum>>,
} }
@@ -211,6 +213,7 @@ impl FrontendRuntimeContext {
remote_rigs: Arc::new(Mutex::new(Vec::new())), remote_rigs: Arc::new(Mutex::new(Vec::new())),
owner_callsign: None, owner_callsign: None,
owner_website_url: None, owner_website_url: None,
owner_website_name: None,
spectrum: Arc::new(Mutex::new(SharedSpectrum::default())), spectrum: Arc::new(Mutex::new(SharedSpectrum::default())),
} }
} }
@@ -1266,6 +1266,7 @@ let serverBuildDate = null;
let serverCallsign = null; let serverCallsign = null;
let ownerCallsign = null; let ownerCallsign = null;
let ownerWebsiteUrl = null; let ownerWebsiteUrl = null;
let ownerWebsiteName = null;
let serverRigs = []; let serverRigs = [];
let serverActiveRigId = null; let serverActiveRigId = null;
let serverLat = null; let serverLat = null;
@@ -1284,7 +1285,7 @@ function updateTitle() {
const titleEl = document.getElementById("rig-title"); const titleEl = document.getElementById("rig-title");
if (titleEl) { if (titleEl) {
if (ownerWebsiteUrl) { if (ownerWebsiteUrl) {
const label = ownerCallsign || displayLabelFromUrl(ownerWebsiteUrl); const label = ownerWebsiteName || displayLabelFromUrl(ownerWebsiteUrl);
titleEl.innerHTML = titleEl.innerHTML =
`<a class="title-link" href="${escapeMapHtml(ownerWebsiteUrl)}" target="_blank" rel="noopener">${escapeMapHtml(label)}</a>`; `<a class="title-link" href="${escapeMapHtml(ownerWebsiteUrl)}" target="_blank" rel="noopener">${escapeMapHtml(label)}</a>`;
} else { } else {
@@ -1314,6 +1315,9 @@ function render(update) {
if (typeof update.owner_website_url === "string" && update.owner_website_url.length > 0) { if (typeof update.owner_website_url === "string" && update.owner_website_url.length > 0) {
ownerWebsiteUrl = update.owner_website_url; ownerWebsiteUrl = update.owner_website_url;
} }
if (typeof update.owner_website_name === "string" && update.owner_website_name.length > 0) {
ownerWebsiteName = update.owner_website_name;
}
if (update.server_latitude != null) serverLat = update.server_latitude; if (update.server_latitude != null) serverLat = update.server_latitude;
if (update.server_longitude != null) serverLon = update.server_longitude; if (update.server_longitude != null) serverLon = update.server_longitude;
if (typeof update.initial_map_zoom === "number" && Number.isFinite(update.initial_map_zoom)) { if (typeof update.initial_map_zoom === "number" && Number.isFinite(update.initial_map_zoom)) {
@@ -37,6 +37,7 @@ struct FrontendMeta {
rig_ids: Vec<String>, rig_ids: Vec<String>,
owner_callsign: Option<String>, owner_callsign: Option<String>,
owner_website_url: Option<String>, owner_website_url: Option<String>,
owner_website_name: Option<String>,
show_sdr_gain_control: bool, show_sdr_gain_control: bool,
initial_map_zoom: u8, initial_map_zoom: u8,
} }
@@ -86,6 +87,9 @@ fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String {
if let Some(url) = meta.owner_website_url { if let Some(url) = meta.owner_website_url {
map.insert("owner_website_url".to_string(), serde_json::json!(url)); map.insert("owner_website_url".to_string(), serde_json::json!(url));
} }
if let Some(name) = meta.owner_website_name {
map.insert("owner_website_name".to_string(), serde_json::json!(name));
}
map.insert( map.insert(
"show_sdr_gain_control".to_string(), "show_sdr_gain_control".to_string(),
serde_json::json!(meta.show_sdr_gain_control), serde_json::json!(meta.show_sdr_gain_control),
@@ -110,6 +114,7 @@ fn frontend_meta_from_context(
rig_ids: rig_ids_from_context(context), rig_ids: rig_ids_from_context(context),
owner_callsign: owner_callsign_from_context(context), owner_callsign: owner_callsign_from_context(context),
owner_website_url: owner_website_url_from_context(context), owner_website_url: owner_website_url_from_context(context),
owner_website_name: owner_website_name_from_context(context),
show_sdr_gain_control: show_sdr_gain_control_from_context(context), show_sdr_gain_control: show_sdr_gain_control_from_context(context),
initial_map_zoom: initial_map_zoom_from_context(context), initial_map_zoom: initial_map_zoom_from_context(context),
} }
@@ -149,6 +154,10 @@ fn owner_website_url_from_context(context: &FrontendRuntimeContext) -> Option<St
context.owner_website_url.clone() context.owner_website_url.clone()
} }
fn owner_website_name_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context.owner_website_name.clone()
}
fn show_sdr_gain_control_from_context(context: &FrontendRuntimeContext) -> bool { fn show_sdr_gain_control_from_context(context: &FrontendRuntimeContext) -> bool {
context.http_show_sdr_gain_control context.http_show_sdr_gain_control
} }