Skip to content

milestone: Working clinical OHIF viewer on one-origin compose stack#53

Open
3clyp50 wants to merge 11 commits intoTerminallyLazy:new-dev-1-31-26from
3clyp50:new-dev-1-31-26
Open

milestone: Working clinical OHIF viewer on one-origin compose stack#53
3clyp50 wants to merge 11 commits intoTerminallyLazy:new-dev-1-31-26from
3clyp50:new-dev-1-31-26

Conversation

@3clyp50
Copy link
Contributor

@3clyp50 3clyp50 commented Mar 9, 2026

This PR stabilizes the governed clinical path (login → worklist → /viewer) when running the full stack under Docker Compose on native Linux. It splits backend dependencies into clinical vs research, fixes OHIF routing and DICOMweb wiring under the /viewer basename, ensures Orthanc exposes DICOMweb correctly, and switches all DICOM STOW uploads to multipart/related so the live Orthanc endpoint accepts them. The RadSysX workspace panel now mounts and the end-to-end flow is validated at http://localhost:3000.


Changes by area

Backend: clinical vs research dependencies

  • backend/requirements-clinical.txt (new): Minimal dependency set for governed clinical bootstrap and compose. Used by docker-compose.yml and recommended for local clinical-only runs.
  • backend/requirements.txt: Remains the broader research/agent set; reorganized and updated.
  • backend/server.py: Optional MCP/research imports are wrapped so missing packages do not block clinical startup. Clinical mode can start with only requirements-clinical.txt installed.
  • dev.sh, docker-compose.yml: Use root .venv and the clinical requirements path where appropriate.
  • Docs: AGENTS.md, README.md, WARP.md, CLAUDE.md, PHASE4_CLINICAL_EXECUTION_CHECKLIST.md updated with Linux bootstrap, clinical vs research install paths, and Python 3.12 as the shared baseline when both stacks are needed on one host.

Viewer: OHIF mode and routing

  • viewer/assets/radsysx-ohif-mode.js:
    • routeName set from "viewer" to "" so the clinical route is mounted at the router basename root. With routerBasename="/viewer", using "viewer" prevented the mode route from rendering under /viewer.
    • Workspace panel activation: onModeEnter now calls panelService.activatePanel(workspacePanel, true) so the RadSysX workspace panel becomes active and the custom element mounts (fixing the stuck loader).
    • Extension registration: Removed the extensions entry for @radsysx/extension-clinical from the mode config; the extension is injected globally via window.__RADSYSX_OHIF_EXTENSION__, and the module specifier caused runtime resolution failures.

Viewer: bootstrap and URL handling

  • viewer/assets/radsysx-bootstrap.js:
    • DOM readiness: ensureLoader() is async and waits for document.body (or DOMContentLoaded) before creating the loader, avoiding null-body failures when the script runs early.
    • Same-origin DICOMweb roots: applyResolvedViewerRuntime() now normalizes qidoRoot, wadoRoot, wadoUriRoot, and stowRoot through normalizeSameOriginUrl() so root-relative paths (e.g. /dicom-web) become absolute same-origin URLs. Prevents path-relative request bugs in the live OHIF runtime.
    • URL cleanup: stripSensitiveQuery() also removes _rsc so routes that bypass Next.js do not leave RSC params in the viewer URL (commit cab2a24).

Viewer: extension data source and DICOMweb

  • viewer/assets/radsysx-ohif-extension.js:
    • Same-origin normalization: applyViewerRuntimeToDicomwebConfiguration() applies normalizeSameOriginUrl() to all DICOMweb roots before applying them to the data source configuration (aligned with bootstrap behavior).
    • Study UID fallback: Logic for falling back to launch context study UID when no study instance UIDs are returned is clarified (same behavior, clearer branching).

Viewer: build and assets

  • viewer/scripts/build-ohif-dist.mjs:
    • OHIF dist discovery: Looks for @ohif/app dist under both viewer/node_modules and workspace root node_modules so the build works from either location (path hotfix 0110a9e).
    • Dynamic base href: Injected script in index.html sets window.__RADSYSX_VIEWER_BASE_PATH__ and window.__RADSYSX_PUBLIC_URL__ from the current pathname and writes a <base href> so assets resolve correctly under /viewer/.
    • Runtime public path: Patches OHIF bundles to replace __webpack_require__.p = "/" with a function that uses __RADSYSX_PUBLIC_URL__ / pathname-derived base so workers and lazy-loaded chunks load under /viewer/.
    • CSS and manifest: patchCssAssetUrls() normalizes url(/...) in CSS; patchManifest() and service worker patches normalize asset/precache URLs for the viewer base path.
    • Logo: Copies RadSysX-Logo.png from workspace root into dist as radsysx-logo.png and adds whiteLabeling.createLogoComponentFn in app config so the OHIF shell shows the RadSysX logo.

Orthanc and DICOMweb

  • deploy/clinical-stack/orthanc.json: Plugins array added to explicitly load libOrthancDicomWeb.so. The library was present in the image but the running instance did not load it, so /dicom-web/* returned "Unknown resource" until the plugin was loaded.
  • backend/clinical/dicomweb.py: store_uploaded_instances() now always uses multipart/related; type="..."; boundary=... for STOW (no single-file application/dicom). Orthanc’s DICOMweb endpoint was returning 415 for single-part uploads.
  • backend/clinical/seed_orthanc.py: Seed uploads use the same multipart/related format via a new build_multipart_related() helper so seeded studies are accepted by Orthanc and appear in QIDO-RS.

Compose and docs

  • docker-compose.yml: Orthanc runs with the rendered config script as entrypoint (single config path). Backend and orthanc-seed use clinical env and DICOMweb URLs. Docs state that governed clinical validation must use the nginx-served origin (http://localhost:3000); the raw viewer dev server on port 3001 is documented as an internal asset server, not a supported clinical entry point.
  • Launch URLs: Viewer URL in launch response and tests use the configured viewer base (e.g. http://localhost:3000/viewer/?launch=...); no PHI in URLs (governed launch contract).
  • .gitignore: Updates for local/IDE and build artifacts.

Technical decisions

Decision Rationale
routeName: "" for clinical mode With routerBasename="/viewer", the mode route must be the basename root so OHIF renders under /viewer.
Remove mode extensions entry for RadSysX Extension is injected via window.__RADSYSX_OHIF_EXTENSION__; an importable module specifier caused resolution failures.
Normalize DICOMweb roots to absolute same-origin URLs Root-relative values were treated as path-relative in the runtime; normalizing to origin + path fixes QIDO/WADO/STOW requests.
Explicit Orthanc plugin load DicomWeb.Enable in config is not enough; the plugin must be listed in Plugins for /dicom-web/* to work.
Always use multipart/related for STOW Orthanc DICOMweb rejects single-file application/dicom with 415; multipart is required for both seed and derived-result uploads.
Force-activate workspace panel in onModeEnter Panel was registered but not active, so the custom element did not mount and the loader never cleared.

How to verify

  1. Compose stack: From repo root, set RADSYSX_ORTHANC_USERNAME and RADSYSX_ORTHANC_PASSWORD, then docker compose up --build. Use http://localhost:3000 (nginx).
  2. Clinical flow: Login → open /worklist → launch a study → open the viewer URL → confirm OHIF viewport loads and the RadSysX Workspace panel appears in the right sidebar and loader clears.
  3. Backend tests: pytest backend/tests/test_clinical_platform.py (launch resolution, same-origin roots, STOW registration).
  4. Viewer build: npm run build --workspace viewer (ensure viewer/dist is not root-owned if you ran compose with bind mounts; otherwise host build may fail). If necessary, when build fails remove the dist folder and retry.

Notes

  • Logo: RadSysX-Logo.png is added at repo root and copied into viewer dist by build-ohif-dist.mjs; the OHIF app config uses it for whiteLabeling.createLogoComponentFn.
  • Handoff: .handoffs/handoff-radsysx-20260309-014811.md is a session handoff document; you may exclude it from the PR or keep it for context.
  • Host viewer/dist: After container rebuilds, viewer/dist can be root-owned on the host; fix with chown or remove before running npm run build --workspace viewer locally.

3clyp50 and others added 10 commits March 8, 2026 22:41
- add backend/requirements-clinical.txt for governed clinical bootstrap and compose usage
- keep backend/requirements.txt as the broader research/agent dependency set
- harden backend/server.py so missing optional MCP/research imports do not block clinical startup
- update Linux/bootstrap guidance across AGENTS.md, README.md, WARP.md, CLAUDE.md, and the clinical checklist
- align dev.sh and docker-compose.yml with the root .venv and clinical dependency path
- document Python 3.12 as the shared baseline when one host needs both clinical and research installs
Update .gitignore

update guidance and repo config (monorepo, frontend)

Update requirements.txt

Update requirements.txt
- run Orthanc through the rendered config script as the service entrypoint so the clinical compose stack does not pass multiple config paths at startup
- harden the OHIF bootstrap loader to wait for the DOM before mounting, avoiding null-body failures during early viewer startup
- clarify in README.md and AGENTS.md that governed clinical validation must go through the nginx-served localhost:3000 origin
- document that the raw viewer dev server on port 3001 is an internal asset server, not a supported clinical entry point
Align the clinical OHIF route with the /viewer basename, activate the RadSysX workspace panel on entry, and normalize same-origin DICOMweb roots so the governed viewer can fully initialize under the nginx-served compose stack.

Explicitly load Orthanc's DICOMweb plugin and switch seeded/backend DICOM uploads to multipart STOW so local clinical studies are queryable through /dicom-web and the OHIF workspace loads end to end.
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fe835998-3ef0-41a8-b5b8-15bf0975a189

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link

Review Summary by Qodo

Stabilize governed clinical OHIF viewer on one-origin compose stack with DICOMweb normalization and workspace panel activation

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Stabilize governed clinical OHIF viewer on one-origin compose stack
  - Split backend dependencies into clinical vs research sets
  - Fix OHIF routing and DICOMweb wiring under /viewer basename
  - Ensure Orthanc exposes DICOMweb correctly with explicit plugin loading
  - Switch DICOM STOW uploads to multipart/related format
• Harden viewer bootstrap and URL handling
  - Wait for DOM readiness before mounting loader
  - Normalize same-origin DICOMweb roots to absolute URLs
  - Strip transient query parameters (_rsc) from routes
• Activate RadSysX workspace panel on OHIF mode entry
  - Force workspace panel activation in onModeEnter callback
  - Remove redundant extension declaration from mode config
• Update documentation and build configuration
  - Align Next.js config for monorepo workspace imports
  - Update bootstrap guidance across AGENTS.md, README.md, WARP.md, CLAUDE.md
  - Document Python 3.12 as shared baseline for dual clinical/research installs
Diagram
flowchart LR
  A["Backend Clinical Deps"] -->|requirements-clinical.txt| B["FastAPI Clinical Server"]
  B -->|opaque launch token| C["OHIF Bootstrap"]
  C -->|normalize DICOMweb roots| D["OHIF Viewer"]
  D -->|activate workspace panel| E["RadSysX Workspace"]
  F["Orthanc DICOMweb"] -->|multipart/related STOW| B
  G["Seed Studies"] -->|multipart/related STOW| F
  H["Next.js Config"] -->|monorepo tracing| I["Frontend Build"]
Loading

Grey Divider

File Changes

1. backend/clinical/dicomweb.py ✨ Enhancement +5/-10

Switch all STOW uploads to multipart/related format

backend/clinical/dicomweb.py


2. backend/clinical/seed_orthanc.py ✨ Enhancement +15/-2

Use multipart/related for seeded DICOM uploads

backend/clinical/seed_orthanc.py


3. backend/clinical/services.py 🐞 Bug fix +17/-2

Normalize viewer launch URLs with trailing slash

backend/clinical/services.py


View more (20)
4. backend/server.py Error handling +58/-30

Harden MCP/research imports for clinical-only startup

backend/server.py


5. backend/tests/test_clinical_platform.py 🧪 Tests +1/-1

Update test expectations for normalized viewer URLs

backend/tests/test_clinical_platform.py


6. backend/requirements-clinical.txt ⚙️ Configuration changes +11/-0

New minimal clinical dependency set for bootstrap

backend/requirements-clinical.txt


7. backend/requirements.txt ⚙️ Configuration changes +30/-28

Reorganize and document research/agent dependency set

backend/requirements.txt


8. frontend/next.config.js ⚙️ Configuration changes +11/-3

Configure monorepo output tracing and external imports

frontend/next.config.js


9. frontend/app/worklist/page.tsx 🐞 Bug fix +1/-1

Use window.location.assign for viewer navigation

frontend/app/worklist/page.tsx


10. frontend/package.json ✨ Enhancement +2/-1

Add webpack dev mode and turbopack variant scripts

frontend/package.json


11. viewer/assets/radsysx-bootstrap.js 🐞 Bug fix +32/-7

Wait for DOM and normalize same-origin DICOMweb URLs

viewer/assets/radsysx-bootstrap.js


12. viewer/assets/radsysx-ohif-extension.js ✨ Enhancement +23/-10

Normalize DICOMweb roots and clarify study UID fallback

viewer/assets/radsysx-ohif-extension.js


13. viewer/assets/radsysx-ohif-mode.js 🐞 Bug fix +7/-6

Set routeName to empty string and force workspace activation

viewer/assets/radsysx-ohif-mode.js


14. viewer/scripts/build-ohif-dist.mjs ✨ Enhancement +159/-0

Patch OHIF dist for viewer basename and asset URLs

viewer/scripts/build-ohif-dist.mjs


15. deploy/clinical-stack/orthanc.json ⚙️ Configuration changes +3/-0

Explicitly load DICOMweb plugin in Orthanc config

deploy/clinical-stack/orthanc.json


16. docker-compose.yml ⚙️ Configuration changes +4/-2

Use clinical requirements and fix Orthanc entrypoint

docker-compose.yml


17. dev.sh ⚙️ Configuration changes +4/-4

Update venv path and clinical dependency references

dev.sh


18. AGENTS.md 📝 Documentation +18/-5

Document clinical bootstrap and Python 3.12 baseline

AGENTS.md


19. CLAUDE.md 📝 Documentation +7/-2

Update Python baseline and clinical runtime guidance

CLAUDE.md


20. PHASE4_CLINICAL_EXECUTION_CHECKLIST.md 📝 Documentation +3/-1

Add clinical dependency and Python version guidance

PHASE4_CLINICAL_EXECUTION_CHECKLIST.md


21. README.md 📝 Documentation +46/-3

Document clinical workflow, Python baseline, and compose stack

README.md


22. WARP.md 📝 Documentation +13/-1

Add Python 3.12 baseline and full backend install guidance

WARP.md


23. .handoffs/handoff-radsysx-20260309-014811.md 📝 Documentation +135/-0

Checkpoint handoff documenting clinical viewer stabilization

.handoffs/handoff-radsysx-20260309-014811.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 9, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Browser STOW targets /dicom-web📘 Rule violation ⛨ Security
Description
The viewer runtime wiring sets the OHIF DICOMweb stowRoot from clinical viewerRuntime, which
defaults to /dicom-web (Orthanc), enabling direct browser STOW writes to Orthanc in governed
clinical flows. This violates the requirement to mediate clinical writeback via backend contracts
instead of browser-to-Orthanc DICOMweb calls.
Code

viewer/assets/radsysx-bootstrap.js[141]

+    dicomwebSource.configuration.stowRoot = normalizeSameOriginUrl(runtime.stowRoot);
Evidence
Compliance ID 7 forbids browser direct writes to Orthanc/DICOMweb in governed flows. The PR modifies
viewer code to apply viewerRuntime.stowRoot into the OHIF DICOMweb datasource, while the backend
runtime derives stowRoot from settings that default to /dicom-web, i.e., the Orthanc DICOMweb
path.

AGENTS.md
viewer/assets/radsysx-bootstrap.js[138-141]
viewer/assets/radsysx-ohif-extension.js[526-540]
backend/clinical/services.py[333-352]
backend/clinical/config.py[65-84]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The governed clinical viewer config wires `stowRoot` to the Orthanc DICOMweb path (`/dicom-web`), which enables direct browser STOW to Orthanc. Compliance requires clinical writeback (SR/SEG/derived results) to be backend-mediated (e.g., `POST /api/derived-results/stow`) rather than browser-to-Orthanc.
## Issue Context
- Backend emits `viewerRuntime.stowRoot` from `settings.dicomweb_stow_root`, which defaults to `/dicom-web`.
- Viewer bootstrap/extension applies this `stowRoot` into the OHIF dicomweb datasource unconditionally.
- Backend already exposes a backend-mediated STOW contract (`/api/derived-results/stow`) used by the RadSysX panel; the viewer should not additionally enable direct Orthanc STOW in governed modes.
## Fix Focus Areas
- viewer/assets/radsysx-bootstrap.js[124-142]
- viewer/assets/radsysx-ohif-extension.js[526-540]
- backend/clinical/services.py[333-352]
- backend/clinical/config.py[65-84]ক্রান্ত

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. FHIR/MCP path broken🐞 Bug ⛯ Reliability
Description
FHIR/MCP “enabled” startup can be a false-positive: startup runs FHIRMCPServer.initialize via
asyncio.to_thread even though it is async, so it returns a coroutine that is never awaited.
Additionally, the /fhir/tool endpoint calls methods/signatures that FHIRMCPServer does not
implement, so enabling real FHIRMCPServer will still fail at runtime.
Code

backend/server.py[R299-306]

+    if FHIRMCPServer is None:
+        raise RuntimeError(MCP_IMPORT_ERROR or "FHIR/MCP imports are unavailable.")
   fhir_server = FHIRMCPServer()
except Exception as exc:
-    class _FallbackFHIRServer:
-        async def initialize(self):
-            return None
-
-        async def list_resources(self):
-            return {"error": f"FHIR integration unavailable: {exc}"}
-
-        async def get_patient_demographics(self, patient_id: str):
-            return {"error": f"FHIR integration unavailable: {exc}", "patient_id": patient_id}
-
-        async def get_medication_list(self, patient_id: str):
-            return {"error": f"FHIR integration unavailable: {exc}", "patient_id": patient_id}
-
-        async def search_resources(self, resource_type: str, search_params: dict):
-            return {
-                "error": f"FHIR integration unavailable: {exc}",
-                "resource_type": resource_type,
-                "params": search_params,
-            }
-
-    fhir_server = _FallbackFHIRServer()
+    fhir_server = _FallbackFHIRServer(exc)
clinical_repository = ClinicalRepository(settings.clinical_database_url)
clinical_service = ClinicalPlatformService(
   settings=settings,
Evidence
backend/server.py calls await asyncio.to_thread(fhir_server.initialize) during startup when FHIR
is “available”, but FHIRMCPServer.initialize is declared async and performs awaits
internally—calling it in a thread returns a coroutine object without executing it. Separately,
/fhir/tool calls fhir_server.list_resources() with no args and calls methods like
get_patient_demographics that do not exist on FHIRMCPServer; FHIRMCPServer.list_resources requires
positional args.

backend/server.py[312-322]
backend/mcp/fhir_server.py[637-653]
backend/server.py[554-575]
backend/mcp/fhir_server.py[658-664]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
FHIR/MCP integration is currently non-functional when enabled:
1) `startup_event` calls `fhir_server.initialize` via `asyncio.to_thread`, but `FHIRMCPServer.initialize` is `async`, so initialization work never runs.
2) `/fhir/tool` calls methods and signatures that `FHIRMCPServer` doesn’t provide.
### Issue Context
The clinical stack wants MCP/FHIR to be optional, but when it *is* configured, it should initialize correctly and its HTTP endpoint should not crash.
### Fix Focus Areas
- backend/server.py[312-333]
- backend/server.py[554-585]
- backend/mcp/fhir_server.py[637-685]
### Suggested approach
- Replace `await asyncio.to_thread(fhir_server.initialize)` with logic that:
- `await fhir_server.initialize()` if it’s a coroutine function
- otherwise runs it in a thread.
- Update `/fhir/tool` to match the real server API (e.g., require `uri_pattern` and `mime_type`, remove unsupported tool methods, or implement them via `read_resource`/FHIR queries), OR wrap `FHIRMCPServer` with an adapter exposing `get_patient_demographics`, etc.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Brittle dist patching🐞 Bug ⛯ Reliability
Description
The viewer build rewrites copied OHIF bundles by searching for an exact string
(__webpack_require__.p = "/";) and hard-fails if it’s not found. This will break viewer builds
entirely when upstream OHIF dist changes formatting/minification of the webpack runtime.
Code

viewer/scripts/build-ohif-dist.mjs[R183-229]

+function patchRuntimePublicPath() {
+  const bundlePaths = fs
+    .readdirSync(distRoot)
+    .filter((fileName) => fileName.endsWith(".js"))
+    .map((fileName) => path.join(distRoot, fileName));
+  const current = '__webpack_require__.p = "/";';
+  const next = `__webpack_require__.p = ((function resolveRadSysXPublicUrl() {
+  const explicit =
+    (typeof self !== "undefined" && (self.__RADSYSX_PUBLIC_URL__ || self.PUBLIC_URL)) ||
+    (typeof window !== "undefined" && (window.__RADSYSX_PUBLIC_URL__ || window.PUBLIC_URL));
+  if (explicit) {
+    return explicit;
+  }
+
+  const locationHref =
+    (typeof self !== "undefined" && self.location && self.location.href) ||
+    (typeof window !== "undefined" && window.location && window.location.href) ||
+    "";
+  if (!locationHref) {
+    return "/";
+  }
+
+  try {
+    const url = new URL(locationHref);
+    const pathname = url.pathname || "/";
+    const basePath = pathname.endsWith("/") ? pathname : pathname.replace(/[^/]*$/, "");
+    return basePath || "/";
+  } catch {
+    return "/";
+  }
+})());`;
+  let patchedCount = 0;
+
+  for (const bundlePath of bundlePaths) {
+    const bundle = fs.readFileSync(bundlePath, "utf8");
+    if (!bundle.includes(current)) {
+      continue;
+    }
+
+    fs.writeFileSync(bundlePath, bundle.replaceAll(current, next), "utf8");
+    patchedCount += 1;
+  }
+
+  if (patchedCount === 0) {
+    throw new Error("Unable to patch OHIF runtime public path in copied dist bundles.");
+  }
+}
Evidence
The build script scans every .js file in dist and only patches those containing the exact current
string. If no file matches, it throws, and viewer/package.json uses this script as the build step—so
the entire viewer build fails.

viewer/scripts/build-ohif-dist.mjs[183-228]
viewer/package.json[6-9]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The viewer build depends on brittle string matching inside upstream OHIF dist bundles to patch webpack’s public path. Minor upstream changes can cause patchedCount==0 and break the viewer build.
### Issue Context
This script is the `npm run build` for the viewer workspace; failures block local dev and compose rebuilds.
### Fix Focus Areas
- viewer/scripts/build-ohif-dist.mjs[183-229]
- viewer/package.json[6-10]
### Suggested approach
- Replace the exact `includes(current)` check with a regex (e.g. `/__webpack_require__\.p\s*=\s*[&amp;quot;&amp;#x27;]\/[&amp;quot;&amp;#x27;];/`).
- Consider patching only runtime/main bundles (exclude `sw.js`, `init-service-worker.js`, and RadSysX-injected scripts).
- If no match is found, emit a diagnostic that prints the OHIF version and a short snippet search result instead of a generic throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Launch URL drops query🐞 Bug ✓ Correctness
Description
_build_viewer_launch_url overwrites any existing query string on RADSYSX_VIEWER_BASE_URL when adding
the launch token. Deployments that rely on fixed query params in viewer_base_url will silently lose
them.
Code

backend/clinical/services.py[R354-367]

+    def _build_viewer_launch_url(self, launch_token: str) -> str:
+        split = urlsplit(self._settings.viewer_base_url.strip())
+        path = split.path or "/"
+        normalized_path = path if path.endswith("/") else f"{path}/"
+        query = f"launch={quote(launch_token, safe='')}"
+        return urlunsplit(
+            (
+                split.scheme,
+                split.netloc,
+                normalized_path,
+                query,
+                split.fragment,
+            )
+        )
Evidence
The function takes split = urlsplit(viewer_base_url) but always returns a urlunsplit with `query =
launch=..., ignoring split.query`. The setting is configurable via RADSYSX_VIEWER_BASE_URL.

backend/clinical/services.py[354-367]
backend/clinical/config.py[21-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Viewer launch URL construction drops any preconfigured query params on `RADSYSX_VIEWER_BASE_URL`.
### Issue Context
Some deployments may need static query params (feature flags, telemetry knobs, etc.) on the viewer entry URL.
### Fix Focus Areas
- backend/clinical/services.py[354-367]
### Suggested approach
- Parse existing query params from `split.query`.
- Set/override `launch`.
- Re-encode the merged query with `urlencode(..., doseq=True)`.
- Keep the current trailing-slash normalization behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. STOW contentType unvalidated🐞 Bug ⛨ Security
Description
The STOW gateway accepts contentType from a form field and interpolates it into the outbound
multipart Content-Type header’s type="..." parameter. Invalid or unexpected values can cause
request failures or header parsing issues against Orthanc.
Code

backend/clinical/dicomweb.py[R185-189]

+        boundary = f"radsysx-{uuid4().hex}"
+        headers = {
+            "Content-Type": f'multipart/related; type="{request.content_type}"; boundary={boundary}'
+        }
+        content = self._build_multipart_related_stream(boundary, files, request.content_type)
Evidence
backend/server.py exposes content_type via Form(alias="contentType") and passes it into
DerivedResultStowRequest. OrthancDICOMwebAdapter then uses request.content_type directly inside
the outbound header string.

backend/server.py[498-527]
backend/clinical/dicomweb.py[185-190]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
User-supplied `contentType` is used to construct an outbound HTTP `Content-Type` header without validation.
### Issue Context
Even if this isn’t exploitable as header injection in practice, it can lead to hard-to-debug STOW failures when clients send unexpected values.
### Fix Focus Areas
- backend/server.py[498-527]
- backend/clinical/dicomweb.py[185-190]
### Suggested approach
- Add server-side validation for `contentType` (e.g., allow only `application/dicom` and possibly `application/dicom+xml` if needed).
- Normalize to a canonical value and use that for both the multipart `type=` parameter and per-part `Content-Type:`.
- Return a 400 with a clear message when an unsupported type is provided.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly stabilizes the governed clinical OHIF viewer workflow within the Docker Compose stack on native Linux. It refines backend dependency management by separating clinical and research requirements, addresses critical OHIF routing and DICOMweb wiring issues, and ensures Orthanc correctly exposes its DICOMweb services. The changes collectively enable a robust end-to-end clinical flow, from login to study viewing with the RadSysX workspace panel, and improve the overall build and deployment consistency.

Highlights

  • Backend Dependency Management: Introduced backend/requirements-clinical.txt for minimal clinical dependencies, allowing the clinical stack to run without broader research/agent dependencies.
  • OHIF Viewer Routing and Panel Activation: Corrected OHIF viewer routing by setting routeName to "" and ensured the RadSysX workspace panel activates correctly on mode entry.
  • DICOMweb Communication Stabilization: Standardized STOW uploads to always use multipart/related for compatibility with Orthanc and normalized DICOMweb roots to absolute same-origin URLs to prevent request issues.
  • Orthanc Configuration: Explicitly configured Orthanc to load the libOrthancDicomWeb.so plugin, resolving "Unknown resource" errors for DICOMweb endpoints.
  • Viewer Build and Asset Handling: Enhanced the viewer build process to dynamically set the base href, patch public paths for correct asset resolution under /viewer/, and include the RadSysX logo.
  • Documentation Updates: Updated various documentation files (AGENTS.md, CLAUDE.md, PHASE4_CLINICAL_EXECUTION_CHECKLIST.md, README.md, WARP.md) to reflect new Python dependency management, Python 3.12 baseline for mixed environments, and correct viewer URL formats.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .gitignore
    • Added /.venv/ and .venv/ to ignore virtual environments across the repository, and removed *.png from ignore list.
  • .handoffs/handoff-radsysx-20260309-014811.md
    • Added a detailed session handoff document for context and future reference.
  • AGENTS.md
    • Updated Python dependency installation instructions to use requirements-clinical.txt and clarified Python 3.12 usage for mixed environments.
  • CLAUDE.md
    • Updated Python dependency instructions to prefer .venv rooted at package-lock.json and added Python baseline guidance.
  • PHASE4_CLINICAL_EXECUTION_CHECKLIST.md
    • Updated Python dependency instructions for clinical bootstrap and Python 3.12 for mixed environments.
  • README.md
    • Updated Python dependency installation instructions, clarified Python 3.12/3.13 usage, and added a recommended whole-runtime validation order.
  • WARP.md
    • Updated Python dependency instructions to use requirements-clinical.txt and added guidance for full backend/runtime installation.
  • backend/clinical/dicomweb.py
    • Modified the store_uploaded_instances function to always use multipart/related content type for STOW requests.
  • backend/clinical/seed_orthanc.py
    • Added a build_multipart_related helper function and updated the seed process to use multipart/related for DICOM uploads.
  • backend/clinical/services.py
    • Introduced a _build_viewer_launch_url method to ensure viewer URLs have a trailing slash and updated launch_imaging to use this method.
  • backend/requirements-clinical.txt
    • Added a new file defining the minimal Python dependencies required for the governed clinical backend.
  • backend/requirements.txt
    • Updated and reorganized the full set of backend Python dependencies, including comments for specific package decisions.
  • backend/server.py
    • Refactored MCP/FHIR import error handling to gracefully manage missing optional dependencies and introduced a _FallbackFHIRServer class.
  • backend/tests/test_clinical_platform.py
    • Updated the assertion for viewerUrl to expect a trailing slash in the URL.
  • deploy/clinical-stack/orthanc.json
    • Added a Plugins array to explicitly load libOrthancDicomWeb.so, enabling DICOMweb functionality.
  • dev.sh
    • Updated the BACKEND_VENV path to the root .venv and modified the frontend dependency installation command.
  • docker-compose.yml
    • Changed the backend service to install requirements-clinical.txt and updated the Orthanc service to use entrypoint for its config script.
  • frontend/app/worklist/page.tsx
    • Replaced router.push with window.location.assign for navigating to the viewer URL.
  • frontend/next.config.js
    • Configured outputFileTracingRoot to the workspace root and enabled externalDir for sibling package imports.
  • frontend/package.json
    • Added a dev:turbopack script and explicitly set the dev script to use webpack.
  • viewer/assets/radsysx-bootstrap.js
    • Made ensureLoader asynchronous, added _rsc query parameter removal, and normalized DICOMweb roots to absolute same-origin URLs.
  • viewer/assets/radsysx-ohif-extension.js
    • Applied normalizeSameOriginUrl to DICOMweb roots in the data source configuration and clarified study UID fallback logic.
  • viewer/assets/radsysx-ohif-mode.js
    • Set the routeName to an empty string, added logic to activate the workspace panel on mode entry, and removed the explicit extension entry.
  • viewer/scripts/build-ohif-dist.mjs
    • Added logic to copy the RadSysX logo, inject dynamic base href, patch webpack public path, normalize CSS asset URLs, and update service worker precache URLs.
Activity
  • No human activity (comments, reviews, approvals) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request primarily focuses on stabilizing the governed clinical OHIF viewer flow, addressing several key areas. Code changes include updating Python dependency management across documentation and build scripts to introduce a backend/requirements-clinical.txt for a leaner clinical stack, while backend/requirements.txt is refined for the full research/agent runtime. The .gitignore file is updated to reflect these changes. Significant modifications were made to the OHIF viewer's bootstrapping and configuration: radsysx-bootstrap.js now normalizes DICOMweb roots to absolute same-origin URLs and ensures the loader is properly initialized, while radsysx-ohif-mode.js aligns the clinical mode with the /viewer basename, force-activates the workspace panel, and removes an explicit extension entry. The build-ohif-dist.mjs script was enhanced to copy additional assets, inject a dynamic <base href>, and patch various OHIF runtime behaviors (public path, CSS asset URLs, manifest, service worker initialization, and precache URLs) for correct deployment under /viewer/. Backend changes in dicomweb.py and seed_orthanc.py standardize STOW uploads to always use multipart/related, and services.py introduces a new method to construct viewer launch URLs with a trailing slash. The backend/server.py file now includes more robust error handling for FHIR/MCP module imports, providing a fallback. Frontend navigation in worklist/page.tsx was updated to use window.location.assign for viewer launches, and next.config.js was configured for monorepo compatibility. Review comments highlight critical security vulnerabilities: patchRuntimePublicPath in build-ohif-dist.mjs is susceptible to an open redirect/arbitrary code execution via crafted URLs, and store_uploaded_instances in backend/clinical/dicomweb.py is vulnerable to HTTP header injection due to direct use of user-supplied content types. Additionally, a normalizeSameOriginUrl function in radsysx-ohif-extension.js is noted as a duplicate and should be refactored for better maintainability.

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