From f607feaec467f33b64c2281956e0f175145691e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 06:09:32 +0000 Subject: [PATCH] [fix](trx-wefax): populate WEFAX tab with working live canvas and gallery - Fix WefaxProgress.line_data serialization: change from Vec (JSON array) to base64-encoded String so the browser's atob() call works - Set output_dir in server WefaxConfig to $XDG_CACHE_HOME/trx-rs/wefax so decoded PNG images are persisted to disk - Add /images/{filename} GET route in trx-frontend-http to serve saved WEFAX PNGs with path traversal protection - Capture live canvas as data URI on image completion for immediate gallery thumbnail display without requiring the file serving route https://claude.ai/code/session_01V1kLpgLPb8Q5wSv4UrcLbr Signed-off-by: Claude --- Cargo.lock | 1 + src/decoders/trx-wefax/Cargo.toml | 1 + src/decoders/trx-wefax/src/decoder.rs | 5 +++- .../assets/web/plugins/wefax.js | 19 ++++++++++---- .../trx-frontend-http/src/api/assets.rs | 25 +++++++++++++++++++ .../trx-frontend-http/src/api/mod.rs | 1 + src/trx-core/src/decode.rs | 2 +- src/trx-server/src/audio.rs | 9 ++++++- 8 files changed, 55 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bd25ea..6e670a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3265,6 +3265,7 @@ dependencies = [ name = "trx-wefax" version = "0.1.0" dependencies = [ + "base64", "png", "trx-core", ] diff --git a/src/decoders/trx-wefax/Cargo.toml b/src/decoders/trx-wefax/Cargo.toml index 1db01f4..02b3741 100644 --- a/src/decoders/trx-wefax/Cargo.toml +++ b/src/decoders/trx-wefax/Cargo.toml @@ -9,4 +9,5 @@ edition = "2021" [dependencies] trx-core = { path = "../../trx-core" } +base64 = "0.22" png = "0.17" diff --git a/src/decoders/trx-wefax/src/decoder.rs b/src/decoders/trx-wefax/src/decoder.rs index 9fcff8c..890eebc 100644 --- a/src/decoders/trx-wefax/src/decoder.rs +++ b/src/decoders/trx-wefax/src/decoder.rs @@ -9,6 +9,7 @@ use std::path::PathBuf; +use base64::Engine; use trx_core::decode::{WefaxMessage, WefaxProgress}; use crate::config::WefaxConfig; @@ -174,6 +175,8 @@ impl WefaxDecoder { .last_line() .map(|l| l.to_vec()) .unwrap_or_default(); + let b64 = base64::engine::general_purpose::STANDARD + .encode(&line_data); events.push(WefaxEvent::Progress( WefaxProgress { rig_id: None, @@ -181,7 +184,7 @@ impl WefaxDecoder { lpm, ioc, pixels_per_line: WefaxConfig::pixels_per_line(ioc), - line_data: Some(line_data.clone()), + line_data: Some(b64), }, line_data, )); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wefax.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wefax.js index 15f63f1..5594b0f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wefax.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wefax.js @@ -82,9 +82,15 @@ function renderGalleryThumbnail(msg) { var ts = msg._tsMs ? new Date(msg._tsMs).toLocaleString() : '\u2014'; var info = msg.ioc + ' IOC \u00b7 ' + msg.lpm + ' LPM \u00b7 ' + msg.line_count + ' lines'; - if (msg.path) { + var imgSrc = msg._dataUrl + ? msg._dataUrl + : msg.path + ? '/images/' + escapeHtml(msg.path.split('/').pop()) + : null; + + if (imgSrc) { card.innerHTML = - 'WEFAX' + '
' + escapeHtml(ts) + '
' + @@ -141,16 +147,19 @@ window.onServerWefaxProgress = function (msg) { window.onServerWefax = function (msg) { msg._tsMs = msg.ts_ms || Date.now(); - wefaxImageHistory.unshift(msg); - pruneWefaxHistory(); - scheduleWefaxGalleryRender(); + // Capture the live canvas as a data URI for gallery thumbnails. if (wefaxLiveCtx && wefaxLiveLineCount > 0) { var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxLiveCanvas.width, wefaxLiveLineCount); wefaxLiveCanvas.height = wefaxLiveLineCount; wefaxLiveCtx.putImageData(trimmed, 0, 0); + try { msg._dataUrl = wefaxLiveCanvas.toDataURL('image/png'); } catch (e) {} } + wefaxImageHistory.unshift(msg); + pruneWefaxHistory(); + scheduleWefaxGalleryRender(); + if (wefaxStatus) { wefaxStatus.textContent = 'Complete \u2014 ' + msg.line_count + ' lines'; wefaxStatus.style.color = ''; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs index 83e214a..f9f187c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs @@ -5,6 +5,7 @@ //! Static asset serving endpoints (HTML pages, JS, CSS, favicon, logo). use actix_web::http::header; +use actix_web::web; use actix_web::{get, HttpRequest, HttpResponse, Responder}; use std::sync::OnceLock; @@ -336,6 +337,30 @@ pub(crate) async fn wefax_js(req: HttpRequest) -> impl Responder { ) } +#[get("/images/{filename}")] +pub(crate) async fn wefax_image(path: web::Path) -> impl Responder { + let filename = path.into_inner(); + // Reject path traversal attempts. + if filename.contains('/') || filename.contains('\\') || filename.contains("..") { + return HttpResponse::BadRequest().body("invalid filename"); + } + if !filename.ends_with(".png") { + return HttpResponse::BadRequest().body("only .png files are accessible"); + } + let dir = dirs::cache_dir() + .unwrap_or_else(|| std::path::PathBuf::from(".cache")) + .join("trx-rs") + .join("wefax"); + let file_path = dir.join(&filename); + match std::fs::read(&file_path) { + Ok(data) => HttpResponse::Ok() + .insert_header((header::CONTENT_TYPE, "image/png")) + .insert_header((header::CACHE_CONTROL, "public, max-age=86400")) + .body(data), + Err(_) => HttpResponse::NotFound().body("image not found"), + } +} + #[get("/bookmarks.js")] pub(crate) async fn bookmarks_js(req: HttpRequest) -> impl Responder { let c = gz_bookmarks_js(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs index ba7697d..c0654c2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs @@ -664,6 +664,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(assets::cw_js) .service(assets::sat_js) .service(assets::wefax_js) + .service(assets::wefax_image) .service(assets::bookmarks_js) .service(assets::scheduler_js) .service(assets::sat_scheduler_js) diff --git a/src/trx-core/src/decode.rs b/src/trx-core/src/decode.rs index f10e42d..7a4e15f 100644 --- a/src/trx-core/src/decode.rs +++ b/src/trx-core/src/decode.rs @@ -310,5 +310,5 @@ pub struct WefaxProgress { pub pixels_per_line: u16, /// Base64-encoded greyscale line data (one row of pixels). #[serde(skip_serializing_if = "Option::is_none")] - pub line_data: Option>, + pub line_data: Option, } diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 1f4dcd6..73fdcb4 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -2692,7 +2692,14 @@ pub async fn run_wefax_decoder( sample_rate, channels ); - let config = WefaxConfig::default(); + let wefax_output_dir = dirs::cache_dir() + .unwrap_or_else(|| std::path::PathBuf::from(".cache")) + .join("trx-rs") + .join("wefax"); + let config = WefaxConfig { + output_dir: Some(wefax_output_dir.to_string_lossy().into_owned()), + ..WefaxConfig::default() + }; let mut decoder = WefaxDecoder::new(sample_rate, config); let mut was_active = false; let mut last_reset_seq: u64 = 0;