[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 {
|
pub struct GeneralConfig {
|
||||||
/// Callsign or owner label to display in frontends
|
/// Callsign or owner label to display in frontends
|
||||||
pub callsign: Option<String>,
|
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)
|
/// Log level (trace, debug, info, warn, error)
|
||||||
pub log_level: Option<String>,
|
pub log_level: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -45,6 +47,7 @@ impl Default for GeneralConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
callsign: Some("N0CALL".to_string()),
|
callsign: Some("N0CALL".to_string()),
|
||||||
|
website_url: None,
|
||||||
log_level: None,
|
log_level: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,6 +337,11 @@ impl ClientConfig {
|
|||||||
return Err("[remote.auth].token must not be empty when set".to_string());
|
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 {
|
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());
|
||||||
@@ -424,6 +432,7 @@ impl ClientConfig {
|
|||||||
let example = ClientConfig {
|
let example = ClientConfig {
|
||||||
general: GeneralConfig {
|
general: GeneralConfig {
|
||||||
callsign: Some("N0CALL".to_string()),
|
callsign: Some("N0CALL".to_string()),
|
||||||
|
website_url: Some("https://haxx.space".to_string()),
|
||||||
log_level: Some("info".to_string()),
|
log_level: Some("info".to_string()),
|
||||||
},
|
},
|
||||||
remote: RemoteConfig {
|
remote: RemoteConfig {
|
||||||
@@ -545,6 +554,7 @@ mod tests {
|
|||||||
assert!(config.frontends.http_json.enabled);
|
assert!(config.frontends.http_json.enabled);
|
||||||
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_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);
|
||||||
@@ -559,6 +569,7 @@ mod tests {
|
|||||||
let toml_str = r#"
|
let toml_str = r#"
|
||||||
[general]
|
[general]
|
||||||
callsign = "W1AW"
|
callsign = "W1AW"
|
||||||
|
website_url = "https://example.com"
|
||||||
|
|
||||||
[remote]
|
[remote]
|
||||||
url = "192.168.1.100:9000"
|
url = "192.168.1.100:9000"
|
||||||
@@ -576,6 +587,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.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()));
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
.clone()
|
.clone()
|
||||||
.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();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Starting trx-client (remote: {}, frontends: {})",
|
"Starting trx-client (remote: {}, frontends: {})",
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ pub struct FrontendRuntimeContext {
|
|||||||
pub remote_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
|
pub remote_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
|
||||||
/// Owner callsign from trx-client config/CLI for frontend display.
|
/// Owner callsign from trx-client config/CLI for frontend display.
|
||||||
pub owner_callsign: Option<String>,
|
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.
|
/// Latest spectrum frame from the active SDR rig; None for non-SDR backends.
|
||||||
pub spectrum: Arc<Mutex<SharedSpectrum>>,
|
pub spectrum: Arc<Mutex<SharedSpectrum>>,
|
||||||
}
|
}
|
||||||
@@ -208,6 +210,7 @@ impl FrontendRuntimeContext {
|
|||||||
remote_active_rig_id: Arc::new(Mutex::new(None)),
|
remote_active_rig_id: Arc::new(Mutex::new(None)),
|
||||||
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,
|
||||||
spectrum: Arc::new(Mutex::new(SharedSpectrum::default())),
|
spectrum: Arc::new(Mutex::new(SharedSpectrum::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1265,6 +1265,7 @@ let serverVersion = null;
|
|||||||
let serverBuildDate = null;
|
let serverBuildDate = null;
|
||||||
let serverCallsign = null;
|
let serverCallsign = null;
|
||||||
let ownerCallsign = null;
|
let ownerCallsign = null;
|
||||||
|
let ownerWebsiteUrl = null;
|
||||||
let serverRigs = [];
|
let serverRigs = [];
|
||||||
let serverActiveRigId = null;
|
let serverActiveRigId = null;
|
||||||
let serverLat = null;
|
let serverLat = null;
|
||||||
@@ -1282,11 +1283,26 @@ function updateFooterBuildInfo() {
|
|||||||
function updateTitle() {
|
function updateTitle() {
|
||||||
const titleEl = document.getElementById("rig-title");
|
const titleEl = document.getElementById("rig-title");
|
||||||
if (titleEl) {
|
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);
|
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) {
|
function render(update) {
|
||||||
if (!update) return;
|
if (!update) return;
|
||||||
if (update.server_version) serverVersion = update.server_version;
|
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) {
|
if (typeof update.owner_callsign === "string" && update.owner_callsign.length > 0) {
|
||||||
ownerCallsign = update.owner_callsign;
|
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_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)) {
|
||||||
|
|||||||
@@ -508,6 +508,14 @@ small { color: var(--text-muted); }
|
|||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
}
|
}
|
||||||
.title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; }
|
.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 {
|
.overview-strip {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ struct FrontendMeta {
|
|||||||
active_rig_id: Option<String>,
|
active_rig_id: Option<String>,
|
||||||
rig_ids: Vec<String>,
|
rig_ids: Vec<String>,
|
||||||
owner_callsign: Option<String>,
|
owner_callsign: Option<String>,
|
||||||
|
owner_website_url: Option<String>,
|
||||||
show_sdr_gain_control: bool,
|
show_sdr_gain_control: bool,
|
||||||
initial_map_zoom: u8,
|
initial_map_zoom: u8,
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,9 @@ fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String {
|
|||||||
if let Some(owner) = meta.owner_callsign {
|
if let Some(owner) = meta.owner_callsign {
|
||||||
map.insert("owner_callsign".to_string(), serde_json::json!(owner));
|
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(
|
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),
|
||||||
@@ -105,6 +109,7 @@ fn frontend_meta_from_context(
|
|||||||
active_rig_id: active_rig_id_from_context(context),
|
active_rig_id: active_rig_id_from_context(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),
|
||||||
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),
|
||||||
}
|
}
|
||||||
@@ -140,6 +145,10 @@ fn owner_callsign_from_context(context: &FrontendRuntimeContext) -> Option<Strin
|
|||||||
context.owner_callsign.clone()
|
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 {
|
fn show_sdr_gain_control_from_context(context: &FrontendRuntimeContext) -> bool {
|
||||||
context.http_show_sdr_gain_control
|
context.http_show_sdr_gain_control
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user