Skip to content

feat(wasm-viewer): Save as Report works offline#924

Merged
thinkingfish merged 2 commits into
iopsystems:mainfrom
thinkingfish:feat/wasm-save-as-report
May 13, 2026
Merged

feat(wasm-viewer): Save as Report works offline#924
thinkingfish merged 2 commits into
iopsystems:mainfrom
thinkingfish:feat/wasm-save-as-report

Conversation

@thinkingfish
Copy link
Copy Markdown
Member

Summary

The static-site WASM viewer's Save as Report button was POSTing to /api/v1/save_with_selection. On rezolus.com (a static deployment with no backend) the CDN returned 405 Method Not Allowed. Port the trim + tarball-repack flow into the WASM crate so the save runs entirely in-browser against the bytes already loaded.

Changes

crates/viewer/src/report_save.rs (new) — mirror of src/viewer/report_save.rs operating on Bytes (which the WASM Tsdb already holds) instead of file paths. Same trim/embed semantics, same KEY_REPORT marker, same AB tar layout. Duplicates rather than extracts a shared crate; the surface is small enough that the drift risk is manageable. We can refactor into a workspace crate later if the surface grows.

crates/viewer/src/lib.rsViewer now keeps the original parquet Bytes alongside the Tsdb so the trim writer can project columns from the source schema (the Tsdb has lost Arrow field metadata like the metric key that parquet filter's keep-field predicate needs). WasmCaptureRegistry::save_with_selection(payload_json) dispatches to single-parquet or combined-A/B based on slot attachment. For AB, an AbContainers manifest is synthesized from each slot's alias + source field (the static site loads two separate parquets, never a real tar manifest).

Unified JS surface — both site/viewer/lib/viewer_api.js (WASM) and src/viewer/assets/lib/viewer_api.js (server) gain saveWithSelection(payload) -> { bytes, mime, extension }. saveToParquet in selection.js calls through ViewerApi instead of raw XHR, swapping the filename's .parquet suffix for the reported extension (so AB tarballs land as *.parquet.ab.tar).

Cost

WASM bundle: 3.6 MB → 4.3 MB (parquet writer + tar). The no_asm zstd-sys feature is target-scoped — feature resolution under resolver 2 doesn't propagate it to cargo build --bin rezolus, so the standalone binary's zstd compression keeps its x86 asm path (no native perf regression).

Test plan

  • cargo clippy --all-targets -- -D warnings clean
  • cargo test — 169 passed (server-side helpers untouched)
  • node --test tests/*.mjs — 82 passed
  • bash tests/viewer_smoke.sh — full pass (server viewer unaffected, still POSTs)
  • WASM rebuild produces a binding for wasmcaptureregistry_save_with_selection
  • Manual: load a parquet on rezolus.com/viewer, pin charts, hit Save as Report — download lands locally with no network POST

🤖 Generated with Claude Code

…e crate

The static-site WASM viewer's Save as Report button was POSTing to
`/api/v1/save_with_selection`. On rezolus.com (a static deployment
with no backend) the CDN returned 405 Method Not Allowed. Port the
trim + tarball-repack flow into a shared workspace crate so both the
server viewer and the WASM viewer run the same projection logic.

## Shared crate: `crates/report-save/`

Single source of truth for the trim/embed/tarball flow:
- `ReportPayload` / `ReportEntry` / `Side` payload types
- `resolve_kept_columns` (generic over `T: Deref<Target = Tsdb>`)
- `save_single_parquet(Bytes, …) -> Vec<u8>`
- `save_combined_ab_tarball(Bytes, Bytes, manifest_bytes: &[u8], …) -> Vec<u8>`
- All operating on `metriken_query::Bytes` — the unified primitive
  the server reads paths into and the WASM viewer already holds.
- 10 unit tests covering kept-column resolution + the full round-trip
  for both trim/no-trim and single/AB paths.

The shared crate is manifest-agnostic: it takes pre-serialized
manifest bytes for the AB tar entry, so it doesn't need to know about
either crate's `AbContainers` struct.

## Server-side shim: `src/viewer/report_save.rs`

Reads paths into `Bytes`, serializes the binary crate's `AbContainers`,
delegates to `report-save`. ~70 LOC (was ~250 LOC of trim logic + tests).

## WASM-side shim: `crates/viewer/src/report_save.rs`

Synthesizes an `AbContainers` manifest from the two attached viewers
(compare mode loads two separate parquets — there's no pre-existing
tar manifest), serializes it, delegates to `report-save`. The WASM
crate keeps its own `AbContainers` definition since it doesn't depend
on the rezolus binary crate.

`Viewer` now keeps the original parquet `Bytes` alongside the Tsdb so
the trim writer can project columns from the source schema (the Tsdb
has lost Arrow field metadata like the `metric` key that
`parquet filter`'s keep-field predicate needs).
`WasmCaptureRegistry::save_with_selection(payload_json)` dispatches
based on slot attachment.

## Unified JS surface

Both `site/viewer/lib/viewer_api.js` (WASM) and
`src/viewer/assets/lib/viewer_api.js` (server) expose
`saveWithSelection(payload) -> { bytes, mime, extension }`.
`saveToParquet` in `selection.js` calls through `ViewerApi` instead of
raw XHR — swaps the filename's `.parquet` suffix for the reported
extension so AB tarballs land as `*.parquet.ab.tar`.

## Cost

WASM bundle: 3.6 MB → 4.3 MB (parquet writer + tar). The `no_asm`
zstd-sys feature is target-scoped — resolver 2 doesn't propagate it
to `cargo build --bin rezolus`, so the standalone binary's zstd
keeps its x86 asm path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinkingfish thinkingfish force-pushed the feat/wasm-save-as-report branch from 5c35b42 to 29fae38 Compare May 13, 2026 17:02
Regenerated via the new WASM-side save path to exercise it end-to-end
in the static site. 125799 → 126004 bytes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinkingfish thinkingfish marked this pull request as ready for review May 13, 2026 19:27
@thinkingfish thinkingfish merged commit 7ff9ae0 into iopsystems:main May 13, 2026
12 checks passed
@thinkingfish thinkingfish deleted the feat/wasm-save-as-report branch May 13, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant