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 1abe4a1..75933df 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 @@ -80,15 +80,23 @@ fn encode_spectrum_frame(frame: &trx_core::rig::state::SpectrumData) -> String { format!("{},{},{b64}", frame.center_hz, frame.sample_rate) } +#[derive(serde::Serialize)] struct FrontendMeta { + #[serde(rename = "clients")] http_clients: usize, rigctl_clients: usize, + #[serde(skip_serializing_if = "Option::is_none")] rigctl_addr: Option, + #[serde(skip_serializing_if = "Option::is_none")] active_remote: Option, remotes: Vec, + #[serde(skip_serializing_if = "Option::is_none")] owner_callsign: Option, + #[serde(skip_serializing_if = "Option::is_none")] owner_website_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] owner_website_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] ais_vessel_url_base: Option, show_sdr_gain_control: bool, initial_map_zoom: u8, @@ -98,6 +106,16 @@ struct FrontendMeta { server_connected: bool, } +/// Wrapper that flattens a rig state with frontend meta into a single JSON +/// object, replacing the old string-level splice approach. +#[derive(serde::Serialize)] +struct StateWithMeta<'a> { + #[serde(flatten)] + state: &'a serde_json::Value, + #[serde(flatten)] + meta: &'a FrontendMeta, +} + /// Tracks per-SSE-session rig selection so different browser tabs can /// independently view different rigs without interfering. #[derive(Default)] @@ -166,76 +184,21 @@ pub async fn status_api( .body(json)) } -/// Append frontend meta fields to an already-serialised JSON object string. +/// Merge a rig state JSON string with frontend meta via `#[serde(flatten)]`. /// -/// Avoids a full parse→modify→reserialise cycle (two serde round-trips per -/// event) by working directly at the string level: strip the closing `}`, -/// serialize only the extra fields once, and re-close the object. +/// Parses the state once into a `serde_json::Value`, then serializes the +/// combined `StateWithMeta` wrapper in a single pass — cleaner and faster +/// than the old string-level splice approach. fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String { - let trimmed = json.trim_end(); - let Some(base) = trimmed.strip_suffix('}') else { - return json.to_string(); - }; - - // Build only the extra key-value pairs as a JSON fragment. - let mut extra = serde_json::Map::new(); - extra.insert("clients".into(), serde_json::json!(meta.http_clients)); - extra.insert( - "rigctl_clients".into(), - serde_json::json!(meta.rigctl_clients), - ); - if let Some(v) = meta.rigctl_addr { - extra.insert("rigctl_addr".into(), serde_json::json!(v)); - } - if let Some(v) = meta.active_remote { - extra.insert("active_remote".into(), serde_json::json!(v)); - } - extra.insert("remotes".into(), serde_json::json!(meta.remotes)); - if let Some(v) = meta.owner_callsign { - extra.insert("owner_callsign".into(), serde_json::json!(v)); - } - if let Some(v) = meta.owner_website_url { - extra.insert("owner_website_url".into(), serde_json::json!(v)); - } - if let Some(v) = meta.owner_website_name { - extra.insert("owner_website_name".into(), serde_json::json!(v)); - } - if let Some(v) = meta.ais_vessel_url_base { - extra.insert("ais_vessel_url_base".into(), serde_json::json!(v)); - } - extra.insert( - "show_sdr_gain_control".into(), - serde_json::json!(meta.show_sdr_gain_control), - ); - extra.insert( - "initial_map_zoom".into(), - serde_json::json!(meta.initial_map_zoom), - ); - extra.insert( - "spectrum_coverage_margin_hz".into(), - serde_json::json!(meta.spectrum_coverage_margin_hz), - ); - extra.insert( - "spectrum_usable_span_ratio".into(), - serde_json::json!(meta.spectrum_usable_span_ratio), - ); - extra.insert( - "decode_history_retention_min".into(), - serde_json::json!(meta.decode_history_retention_min), - ); - extra.insert( - "server_connected".into(), - serde_json::json!(meta.server_connected), - ); - - // Serialize the extra map, strip its outer braces, and splice in. - let extra_json = match serde_json::to_string(&extra) { - Ok(s) => s, + let state: serde_json::Value = match serde_json::from_str(json) { + Ok(v) => v, Err(_) => return json.to_string(), }; - // extra_json = {"k":v,...} → strip { and } - let inner = &extra_json[1..extra_json.len() - 1]; - format!("{base},{inner}}}") + let combined = StateWithMeta { + state: &state, + meta: &meta, + }; + serde_json::to_string(&combined).unwrap_or_else(|_| json.to_string()) } fn frontend_meta_from_context(