[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 <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-01 18:12:31 +01:00
parent 5899592b6c
commit e337b6d2de
6 changed files with 53 additions and 1 deletions
+12
View File
@@ -37,6 +37,8 @@ pub struct ClientConfig {
pub struct GeneralConfig {
/// Callsign or owner label to display in frontends
pub callsign: Option<String>,
/// Optional website URL to use as the web UI header title link.
pub website_url: Option<String>,
/// Log level (trace, debug, info, warn, error)
pub log_level: Option<String>,
}
@@ -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()));
+1
View File
@@ -244,6 +244,7 @@ async fn async_init() -> DynResult<AppState> {
.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: {})",
+3
View File
@@ -176,6 +176,8 @@ pub struct FrontendRuntimeContext {
pub remote_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
/// Owner callsign from trx-client config/CLI for frontend display.
pub owner_callsign: Option<String>,
/// Optional website URL for the web UI header title link.
pub owner_website_url: Option<String>,
/// Latest spectrum frame from the active SDR rig; None for non-SDR backends.
pub spectrum: Arc<Mutex<SharedSpectrum>>,
}
@@ -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())),
}
}
@@ -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 =
`<a class="title-link" href="${escapeMapHtml(ownerWebsiteUrl)}" target="_blank" rel="noopener">${escapeMapHtml(label)}</a>`;
} 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)) {
@@ -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;
@@ -36,6 +36,7 @@ struct FrontendMeta {
active_rig_id: Option<String>,
rig_ids: Vec<String>,
owner_callsign: Option<String>,
owner_website_url: Option<String>,
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<Strin
context.owner_callsign.clone()
}
fn owner_website_url_from_context(context: &FrontendRuntimeContext) -> Option<String> {
context.owner_website_url.clone()
}
fn show_sdr_gain_control_from_context(context: &FrontendRuntimeContext) -> bool {
context.http_show_sdr_gain_control
}