[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:
@@ -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()));
|
||||
|
||||
@@ -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: {})",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user