---
title: "2026-08-06 product diffsimscan single viewer"
description: "Collapsed the dashboard patient page's per-DiffsimScan viewers into one viewer with a nested scan > volume sidebar, validated against a fully local vxdata-api stack."
image: "https://docs.virdx.dev/img/virdx-social-card.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.virdx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 2026-08-06 product diffsimscan single viewer

## Work Done

Issue [virdx/mono#313](https://github.com/virdx/mono/issues/313), PR
[virdx/mono#314](https://github.com/virdx/mono/pull/314) (draft, branch
`fix/diffsimscan-single-viewer`).

- Problem: `DiffsimScanCard` rendered one `VolumeBrowser` per `DiffsimScan`. An Essen01
  patient has hundreds of `HistoScan`s and roughly one `DiffsimScan` each, so the patient
  page became an endless widget list and mounted hundreds of Niivue WebGL contexts.
- Change: `VolumeBrowser` gained an optional `groups?: VolumeGroup[]` prop
  (`VolumeGroup` = `{ id, label, volumes }` in `imaging-types.ts`). When set, the rail
  renders collapsible groups instead of a flat list; the flat `volumes` prop stays the
  source of truth for selection, sorting, type filtering, search and prefetch. Rail order
  in grouped mode follows on-screen order so arrow keys and prefetch match the list.
  `DiffsimScanCard` now renders exactly one `VolumeBrowser`. `ImagingStudySection`'s flat
  usage is unchanged.
- Found and fixed a pre-existing race in `VolumeViewer`: the volume-load effect read
  `nvRef` at effect time, but Niivue is attached in a `canvas`-keyed effect that only runs
  after the element mounts. A viewer whose volume is known on first render fired the effect
  with `nvRef === null` and never retried, so the canvas stayed on Niivue's own
  "loading ..." screen until the user clicked another volume. Adding `canvas` to the
  effect's dependency list fixes it. This became the default state once the diffsim rail
  preselects a volume in a single viewer.
- Validation (as the issue demanded, all local): native `vxdata-api` stack via
  `just deploy` (Postgres + MinIO + API on 12300) + `just inject-mock`, then 60 synthetic
  `DiffsimScan`s x 3 `DiffsimVolume`s posted to `/resources/create`, with small synthetic
  NIfTI phantoms uploaded through the API's own `/s3/presign` upload flow. Frontend via
  `vite dev` with a gitignored `.env.local` (`VITE_PLATFORM_PROXY_TARGET=http://127.0.0.1:12300`,
  `VITE_S3_PROXY_TARGET=http://127.0.0.1:12304`, `VITE_PLATFORM_BEARER_TOKEN=vxd_dev_admin`)
  against the already-running `dashboard-api` on 8080. Result: one card reading
  "180 volumes in 61 scans", a two-level rail, 3 `<canvas>` elements on the whole page,
  and volumes rendering in the shared viewer. Screenshot kept locally on the Desktop.
- Static checks: `bunx nx run dashboard-frontend:{typecheck,build,format}` all pass;
  `biome check` on the touched files shows the same 11 pre-existing diagnostics as `main`.

## Lessons Learned: Pitfalls

- `wiki/workstreams/` has no `product/` directory (and most workstreams have only an
  `index.md`, no `sops/`), yet the `begin-work` skill instructs the agent to read
  `wiki/workstreams/<workstream>/index.md` **and** `.../sops/index.md`. For frontend /
  dashboard work there is nothing to read, so the step is a dead end. Either add a
  `product/index.md` stub or make `begin-work` say "read whichever of these exist".
- `apps/vxdata-api`'s `just deploy` fails in two ways that are not documented:
  1. `process-compose`'s own admin HTTP server hardcodes port 8080, which collides with a
     locally running `dashboard-api`. Workaround: run `process-compose ... -p 18080 up`
     instead of `just deploy` (or stop dashboard-api first).
  2. Postgres refuses to start when the checkout path is long: the process-compose recipe
     puts the Unix socket in `$(pwd)/.pg`, and paths over ~103 bytes (e.g. any worktree
     under `~/work/tries/<long-dated-slug>/mono`) exceed the socket-path limit. Symlinking
     to a short path does not help (cwd is resolved physically); the fix is a shorter
     checkout path, or pointing `unix_socket_directories` at `/tmp`.
- `src/vxdata/api/mock/mock_data.py` still emits the diffsim volumes as `Volume` payloads
  parented to the `DiffsimScan`, but the frontend (and the current schema) expect
  `DiffsimVolume` with a `url` field. So `just inject-mock` alone produces a diffsim scan
  with **zero** visible volumes -- easy to mistake for a frontend bug. Worth fixing in the
  mock data.
- `bunx nx run dashboard-frontend:format` reformats an unrelated `--font-sans` declaration
  in `src/styles/globals.css` every time. Revert that file before committing, or fix it once.
- `apps/dashboard-frontend` has ~173 pre-existing `biome check` errors and no wired `lint`
  or `test` target in `project.json`, so "run lint before finishing" cannot mean the whole
  app here; compare per-file diagnostics against `main` instead.

## Lessons Learned: Improvements

- A short SOP for "run the dashboard frontend against a fully local platform stack" would
  have saved the most time. The pieces are scattered: `apps/dashboard-frontend/AGENTS.md`
  documents `VITE_PLATFORM_BEARER_TOKEN` but not that MinIO listens on 12304 while
  `VITE_S3_PROXY_TARGET` defaults to 9000, and `apps/vxdata-api/AGENTS.md` documents
  `just deploy` but not the 8080 collision or the socket-path limit.
- Useful trick worth recording: to get a viewer-testable volume into the local stack, POST
  `/s3/presign` with `direction: "upload"`, PUT the bytes to the returned URL, and use the
  returned `s3://` path as the resource's `url` / `path_nii`. The upload presign chooses the
  key itself, so read it back rather than assuming a layout.
- Headless Chromium (`chrome-headless-shell`) renders Niivue canvases unreliably; use a
  headed browser for any WebGL screenshot validation.

Source: https://docs.virdx.dev/knowledge/inbox/2026-08-06-product-diffsimscan-single-viewer/index.mdx
