[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:
Claude
2026-04-03 06:09:32 +00:00
committed by Stan Grams
parent 716d901d75
commit f607feaec4
8 changed files with 55 additions and 8 deletions
@@ -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 =
'<img src="/images/' + escapeHtml(msg.path.split('/').pop()) + '"' +
'<img src="' + imgSrc + '"' +
' alt="WEFAX" loading="lazy"' +
' style="width:100%; image-rendering:pixelated;" />' +
'<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) {
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 = '';
@@ -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<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")]
pub(crate) async fn bookmarks_js(req: HttpRequest) -> impl Responder {
let c = gz_bookmarks_js();
@@ -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)