Viewer parity (server ↔ WASM)
SkillMonitoring & opsUse when adding or changing what the rezolus viewer exposes — an /api/v1 endpoint, a dashboard section, the metric catalog, metadata/description derivation, or the frontend data those depend on — or when editing src/viewer/ or crates/viewer/, or reviewing a viewer change. The viewer ships as two separate Rust backends (server + WASM) behind one shared frontend; changing one silently diverges them.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Viewer parity (server ↔ WASM) skill
What this skill tells your AI
The instructions your AI receives, as published by iopsystems/rezolus in .claude/skills/viewer-parity/SKILL.md and read by ahel’s review.
Overview
rezolus view ships as two backends behind one shared frontend:
- Server — the
rezolusbinary,src/viewer/*.rs(axum HTTP:routes.rs,metadata.rs,state.rs,actions.rs,source_kind.rs, …). - WASM — the separate
crates/viewercrate,crates/viewer/src/lib.rs, which loads a parquet in-browser for the static GitHub-Pages site. - Shared frontend — most of
src/viewer/assets/lib/**is symlinked file-for-file intosite/viewer/lib/, so both backends serve the same JS/CSS. Two files are NOT symlinks —viewer_api.jsandscript.jsare separately maintained copies insite/viewer/lib/.viewer_api.jsis the backend-adapter seam (server does HTTPfetch; WASM calls into the in-browser registry), so it must differ — which means a change to the server'sviewer_api.jsdoes not propagate to the WASM shell. Editing either → mirror the other copy.- A new shared module under
src/viewer/assets/lib/needs a resolving entry undersite/viewer/lib/(a per-file symlink, or coverage by a parent directory symlink likesite/viewer/lib/embed) — or the deployed static viewer 404s on the import and fails to load entirely. This is now enforced in CI (viewer-symlinks.yml→scripts/check-viewer-symlinks.sh); run it locally before pushing a viewer change. (A missingcharts/boxplot.jssymlink shipped exactly this outage before the guard existed.)
- A new shared module under
crates/viewer cannot depend on the rezolus binary crate — rezolus is
binary-only, with no lib target — so any backend logic both need must live in a
shared crate (dashboard, rez) or be duplicated in lib.rs. That
structural gap is why viewer behavior gets added to one side and silently missing
from the other. It is also why the .rez reader was unreachable from the browser
for as long as it lived in src/: moving it to crates/rez is what let the
static-site viewer open an archive at all.
Two things break in the browser without failing to compile, so watch for them in anything the WASM side will reach:
metriken(andmetriken-exposition) — their registry declares alinkmedistributed slice andlinkmehas no wasm32 implementation. This one IS a compile error, but only in a wasm32 build:cargo check -p rez --no-default-features --target wasm32-unknown-unknownis the CI guard.- Threads —
std::thread::spawncompiles for wasm32 and panics at runtime.
Core principle: a change to what the viewer derives from a loaded recording
is a change to BOTH backends. Shipping it on one is a parity regression, not a
smaller scope. (Real instance: per-source classification was added to the server's
metadata.rs and never mirrored in lib.rs, so simple-capture parquets showed no
source: section in the WASM viewer — see
per-source descriptions
and the simple-capture entry.)
The one legitimate exception
Something is server-only only when the capability structurally can't exist
in the browser: talking to a live agent (/connect), proxy-fetching a URL
(/load_url), server reset, upload plumbing. WASM operates on one
already-loaded parquet. Everything that is a pure derivation from the loaded
recording — sections, metric catalog, metadata, systeminfo, descriptions,
PromQL — exists on both. "It's just the server endpoint" is not an exception; it's
the regression.
Server route ↔ WASM method map
The shared viewer_api.js calls /api/v1/<x> via backendRequest; the server
answers with an axum handler, WASM answers with a lib.rs method routed through a
shim. A new frontend-facing endpoint needs an entry on both sides or WASM 404s.
| Frontend calls | Server (src/viewer/routes.rs) | WASM (crates/viewer/src/lib.rs) |
|---|---|---|
/api/v1/sections, /data/<s>.json | sections_handler, data | get_sections, get_section |
/api/v1/metrics | metrics_handler | metrics |
/api/v1/systeminfo | systeminfo_handler | systeminfo |
/api/v1/file_metadata | file_metadata_handler | file_metadata_json |
/api/v1/metadata | metadata | metadata |
/api/v1/selection | selection_handler | selection |
/api/v1/query, /query_range | instant_query, range_query | query, query_range |
/api/v1/save* | actions::save* | save_with_selection |
| dashboard build (not an endpoint) | metadata::regenerate_dashboards → classify_sources | init_templates / regenerate_combined |
/api/v1/connect, /load_url, /reset | server handlers | none — server-only by nature |
How to keep them in parity
- Put the logic in
dashboard, call it from both shells. The endpoint handler and the WASM method are different shells (axum extractors vs#[wasm_bindgen]), but the logic should be one function in thedashboardcrate that both call — likemetric_catalog::assemble_catalogandmetric_catalog::resolve_descriptions. Byte-identical output falls out for free. Do NOT copy logic intolib.rswhen it can be shared. - When a shell must differ, change both shells in the same PR. Adding a route
handler? Add the matching WASM method. Adding classification/section logic in
metadata.rs? Mirror it ininit_templates. - Most frontend changes hit both automatically (symlinked assets) — but only
if the endpoint they call exists on both backends. Verify the WASM side answers
it. Exception:
viewer_api.jsandscript.jsare per-shell copies, not symlinks — a change to one must be mirrored into the other. - Duplicated files are a hazard. Known hand-mirrored pairs:
report_save.rs(src/viewer/andcrates/viewer/src/), and the frontendviewer_api.js/script.js(src/viewer/assets/lib/andsite/viewer/lib/). An edit to one copy must be mirrored to the other. Prefer folding backend duplication intodashboard; theviewer_api.jssplit is intrinsic (the adapter seam differs).
Tests — so one side can't be silently ignored
Compilation parity is necessary but not sufficient — a WASM build can be green while the WASM viewer produces different (or empty) output.
- Build gate (necessary):
cargo check -p viewer --target wasm32-unknown-unknownand./crates/viewer/build.sh. Catches "forgot to compile the WASM side", never "forgot to implement the behavior". - Behavioral parity (the real gate): for a set of fixture parquets (at least
one Rezolus recording, one service/combined file, and one non-Rezolus simple
capture like
hub.heartbeat.parquet), assert the server endpoint JSON and the WASM method JSON are byte-identical for the same input —sections,metrics,systeminfo,file_metadata,metadata. A Rust test can constructcrates/viewer'sViewerfrom the fixture bytes and diff itsmetrics()/get_sections()output against the server path's output for the same fixture. When you add a derivation, add its fixture to this set — a simple-capture fixture is what would have caught the missing WASMsource:section. - Smoke coverage is server-only today.
tests/viewer_smoke.shexercises only the axum server (see theviewer-smokeskill). Adding a curl+jq check there does not cover WASM. Parity needs the WASM-side check above.
Which side am I forgetting? (pre-PR checklist)
- Did I change what the viewer derives from a recording (sections/catalog/metadata/ descriptions/query)? → it belongs on both backends.
- Is my new logic in
dashboard(shared), or did I write it insrc/viewer/only? If server-only, does WASM need it? (Almost always yes for derivations.) - New
/api/v1/*route the frontend calls? → matchinglib.rsmethod added. - Did I add a fixture-diff parity assertion, or only a server-side smoke check?
- Edited
report_save.rs,viewer_api.js, orscript.js? → mirror the other copy (these are hand-maintained duplicates, not symlinks).
Rationalizations — STOP
| Excuse | Reality |
|---|---|
| "The task said the server endpoint, so WASM is out of scope." | A derivation feature on one backend is a parity regression. Scope is the behavior, which lives on both. |
| "I'll do the WASM side as a separate follow-up." | Follow-ups for parity don't happen; the WASM viewer ships broken meanwhile. Same PR. |
"cargo check -p viewer passes, so WASM is fine." | Compilation ≠ behavior. An empty/wrong WASM result compiles clean. |
| "The frontend is shared, so it just works in both." | Only if the endpoint it calls exists on both. Shared frontend hitting a WASM-missing endpoint = silent 404. |
| "It's a small addition." | Small divergences are the ones that ship — no reviewer notices one missing method. |
Signals
- GitHub stars
- 273
- Forks
- 23
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
viewer-parity- Source
- github.com/iopsystems/rezolus