Skip to content
Back to skills

Viewer Parity

ASecurity

Use 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.

  • 275 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 12, 2026
devopsrustgoshellgitapifrontendbackend

Works with

  • api

Security analysis

A100/100

Scanned September 12, 2026

npx -y skills add iopsystems/rezolus --skill viewer-parity --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Viewer Parity?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Viewer Parity
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/iopsystems-viewer-parity/badge)](https://www.skillsdirectory.com/skills/iopsystems-viewer-parity)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: viewer-parity
description: Use 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.
---

# Viewer parity (server ↔ WASM)

## Overview

`rezolus view` ships as **two backends behind one shared frontend**:

- **Server** — the `rezolus` binary, `src/viewer/*.rs` (axum HTTP: `routes.rs`,
  `metadata.rs`, `state.rs`, `actions.rs`, `source_kind.rs`, …).
- **WASM** — the separate `crates/viewer` crate, `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 into `site/viewer/lib/`, so both backends serve the **same**
  JS/CSS. **Two files are NOT symlinks** — `viewer_api.js` and `script.js` are
  **separately maintained copies** in `site/viewer/lib/`. `viewer_api.js` is the
  backend-adapter seam (server does HTTP `fetch`; WASM calls into the in-browser
  registry), so it *must* differ — which means a change to the server's
  `viewer_api.js` does **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 under `site/viewer/lib/`** (a per-file symlink, or coverage by a
    parent directory symlink like `site/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 missing `charts/boxplot.js`
    symlink shipped exactly this outage before the guard existed.)

`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` (and `metriken-exposition`)** — their registry declares a `linkme`
  distributed slice and `linkme` has 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-unknown` is the CI guard.
- **Threads** — `std::thread::spawn` compiles 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](../../../docs/journal/2026-07-04-per-source-descriptions.md)
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

1. **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 the `dashboard`
   crate that both call — like `metric_catalog::assemble_catalog` and
   `metric_catalog::resolve_descriptions`. Byte-identical output falls out for
   free. Do NOT copy logic into `lib.rs` when it can be shared.
2. **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 in `init_templates`.
3. **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.js` and `script.js` are per-shell copies, not
   symlinks — a change to one must be mirrored into the other.
4. **Duplicated files are a hazard.** Known hand-mirrored pairs: `report_save.rs`
   (`src/viewer/` **and** `crates/viewer/src/`), and the frontend `viewer_api.js` /
   `script.js` (`src/viewer/assets/lib/` **and** `site/viewer/lib/`). An edit to
   one copy must be mirrored to the other. Prefer folding backend duplication into
   `dashboard`; the `viewer_api.js` split 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-unknown`
  and `./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 construct
  `crates/viewer`'s `Viewer` from the fixture bytes and diff its `metrics()` /
  `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 WASM `source:` section.
- **Smoke coverage is server-only today.** `tests/viewer_smoke.sh` exercises only
  the axum server (see the `viewer-smoke` skill). 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 in `src/viewer/` only?
  If server-only, does WASM need it? (Almost always yes for derivations.)
- New `/api/v1/*` route the frontend calls? → matching `lib.rs` method added.
- Did I add a fixture-diff parity assertion, or only a server-side smoke check?
- Edited `report_save.rs`, `viewer_api.js`, or `script.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. |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…