Skip to content

2026-08-06 product diffsimscan single viewer

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.

Work Done

Issue virdx/mono#313, PR virdx/mono#314 (draft, branch fix/diffsimscan-single-viewer).

  • Problem: DiffsimScanCard rendered one VolumeBrowser per DiffsimScan. An Essen01 patient has hundreds of HistoScans 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 DiffsimScans x 3 DiffsimVolumes 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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close