[fix](trx-wefax): populate WEFAX tab with working live canvas and gallery
- Fix WefaxProgress.line_data serialization: change from Vec<u8> (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 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
@@ -3265,6 +3265,7 @@ dependencies = [
|
|||||||
name = "trx-wefax"
|
name = "trx-wefax"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"png",
|
"png",
|
||||||
"trx-core",
|
"trx-core",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
trx-core = { path = "../../trx-core" }
|
trx-core = { path = "../../trx-core" }
|
||||||
|
base64 = "0.22"
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
use trx_core::decode::{WefaxMessage, WefaxProgress};
|
use trx_core::decode::{WefaxMessage, WefaxProgress};
|
||||||
|
|
||||||
use crate::config::WefaxConfig;
|
use crate::config::WefaxConfig;
|
||||||
@@ -174,6 +175,8 @@ impl WefaxDecoder {
|
|||||||
.last_line()
|
.last_line()
|
||||||
.map(|l| l.to_vec())
|
.map(|l| l.to_vec())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD
|
||||||
|
.encode(&line_data);
|
||||||
events.push(WefaxEvent::Progress(
|
events.push(WefaxEvent::Progress(
|
||||||
WefaxProgress {
|
WefaxProgress {
|
||||||
rig_id: None,
|
rig_id: None,
|
||||||
@@ -181,7 +184,7 @@ impl WefaxDecoder {
|
|||||||
lpm,
|
lpm,
|
||||||
ioc,
|
ioc,
|
||||||
pixels_per_line: WefaxConfig::pixels_per_line(ioc),
|
pixels_per_line: WefaxConfig::pixels_per_line(ioc),
|
||||||
line_data: Some(line_data.clone()),
|
line_data: Some(b64),
|
||||||
},
|
},
|
||||||
line_data,
|
line_data,
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -82,9 +82,15 @@ function renderGalleryThumbnail(msg) {
|
|||||||
var ts = msg._tsMs ? new Date(msg._tsMs).toLocaleString() : '\u2014';
|
var ts = msg._tsMs ? new Date(msg._tsMs).toLocaleString() : '\u2014';
|
||||||
var info = msg.ioc + ' IOC \u00b7 ' + msg.lpm + ' LPM \u00b7 ' + msg.line_count + ' lines';
|
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 =
|
card.innerHTML =
|
||||||
'<img src="/images/' + escapeHtml(msg.path.split('/').pop()) + '"' +
|
'<img src="' + imgSrc + '"' +
|
||||||
' alt="WEFAX" loading="lazy"' +
|
' alt="WEFAX" loading="lazy"' +
|
||||||
' style="width:100%; image-rendering:pixelated;" />' +
|
' style="width:100%; image-rendering:pixelated;" />' +
|
||||||
'<div style="font-size:0.8rem; margin-top:0.2rem;">' + escapeHtml(ts) + '</div>' +
|
'<div style="font-size:0.8rem; margin-top:0.2rem;">' + escapeHtml(ts) + '</div>' +
|
||||||
@@ -141,16 +147,19 @@ window.onServerWefaxProgress = function (msg) {
|
|||||||
|
|
||||||
window.onServerWefax = function (msg) {
|
window.onServerWefax = function (msg) {
|
||||||
msg._tsMs = msg.ts_ms || Date.now();
|
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) {
|
if (wefaxLiveCtx && wefaxLiveLineCount > 0) {
|
||||||
var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxLiveCanvas.width, wefaxLiveLineCount);
|
var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxLiveCanvas.width, wefaxLiveLineCount);
|
||||||
wefaxLiveCanvas.height = wefaxLiveLineCount;
|
wefaxLiveCanvas.height = wefaxLiveLineCount;
|
||||||
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
||||||
|
try { msg._dataUrl = wefaxLiveCanvas.toDataURL('image/png'); } catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wefaxImageHistory.unshift(msg);
|
||||||
|
pruneWefaxHistory();
|
||||||
|
scheduleWefaxGalleryRender();
|
||||||
|
|
||||||
if (wefaxStatus) {
|
if (wefaxStatus) {
|
||||||
wefaxStatus.textContent = 'Complete \u2014 ' + msg.line_count + ' lines';
|
wefaxStatus.textContent = 'Complete \u2014 ' + msg.line_count + ' lines';
|
||||||
wefaxStatus.style.color = '';
|
wefaxStatus.style.color = '';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
//! Static asset serving endpoints (HTML pages, JS, CSS, favicon, logo).
|
//! Static asset serving endpoints (HTML pages, JS, CSS, favicon, logo).
|
||||||
|
|
||||||
use actix_web::http::header;
|
use actix_web::http::header;
|
||||||
|
use actix_web::web;
|
||||||
use actix_web::{get, HttpRequest, HttpResponse, Responder};
|
use actix_web::{get, HttpRequest, HttpResponse, Responder};
|
||||||
use std::sync::OnceLock;
|
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<String>) -> 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")]
|
#[get("/bookmarks.js")]
|
||||||
pub(crate) async fn bookmarks_js(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn bookmarks_js(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_bookmarks_js();
|
let c = gz_bookmarks_js();
|
||||||
|
|||||||
@@ -664,6 +664,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(assets::cw_js)
|
.service(assets::cw_js)
|
||||||
.service(assets::sat_js)
|
.service(assets::sat_js)
|
||||||
.service(assets::wefax_js)
|
.service(assets::wefax_js)
|
||||||
|
.service(assets::wefax_image)
|
||||||
.service(assets::bookmarks_js)
|
.service(assets::bookmarks_js)
|
||||||
.service(assets::scheduler_js)
|
.service(assets::scheduler_js)
|
||||||
.service(assets::sat_scheduler_js)
|
.service(assets::sat_scheduler_js)
|
||||||
|
|||||||
@@ -310,5 +310,5 @@ pub struct WefaxProgress {
|
|||||||
pub pixels_per_line: u16,
|
pub pixels_per_line: u16,
|
||||||
/// Base64-encoded greyscale line data (one row of pixels).
|
/// Base64-encoded greyscale line data (one row of pixels).
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub line_data: Option<Vec<u8>>,
|
pub line_data: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2692,7 +2692,14 @@ pub async fn run_wefax_decoder(
|
|||||||
sample_rate, channels
|
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 decoder = WefaxDecoder::new(sample_rate, config);
|
||||||
let mut was_active = false;
|
let mut was_active = false;
|
||||||
let mut last_reset_seq: u64 = 0;
|
let mut last_reset_seq: u64 = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user