ADR-014: Integrated, synchronized study PDF viewer¶
Status¶
In-Review. This records a default-off implementation and staged-rollout design; it does not authorize enabling the feature in any environment.
Context¶
Study review currently exposes the API-projected linkToPdf as an external browser tab/window.
That URL is the established authority: it comes from PdfLinkBuilder and may point either to
SyRF-managed storage or an externally hosted PDF. It may also expire or be revoked independently
of the Angular application. The web application already depends on Mozilla PDF.js 4.10.38
(Apache-2.0) and has a bundled module worker, but its old PDF-tools components are not a
study-review viewer.
Reviewers primarily use desktop and multi-monitor layouts. They need a PDF window that follows the committed next/previous study navigation without making another client-side source of truth for PDF authorization.
Decision¶
- Add a default-off
integratedPdfViewerweb feature flag. Flag-off retains the existing, accessible externalView PDFlink unchanged in both legacy review and AF2. - When enabled, open a named SyRF-owned
/pdf-viewerwindow only from a user gesture. The browser URL contains a random, serializer-neutral per-window capability in its fragment, never a PDF URL or study identifier. It deliberately avoids a hyphenated UUID format because SyRF's global URL serializer encodes UUIDs. - Use an opener-bound
postMessageprotocol, not ambientBroadcastChannelstate. The main window accepts a viewer message only whenoriginis its exact origin,sourceis its current popup reference, and the capability matches. Replies use that exact origin. The viewer ignores every other origin, source and capability. This gives refresh/reconnect a narrow lifecycle without making the study or URL routable. - The main review host remains the sole authority. It projects only its already-authorized
current study's id, title and
linkToPdfinto the viewer after the handshake. A change in current-study state increments a revision and replaces the viewer state; PDF URLs are never persisted in route, storage or query state. - The viewer destroys any active PDF.js loading task/document before accepting a newer revision. It displays explicit loading, unavailable and error states, and provides the existing direct open/download link as an accessible fallback. The integrated viewer receives only same-origin PDF URLs: legacy or externally-hosted URLs remain in that direct fallback path rather than being fetched from a different origin. The popup does not infer, synthesize or refresh a URL; an expired/revoked URL remains an error until the main window authorizes a newer state.
- The viewer sends a short heartbeat. If its opener stops confirming the exact capability (for example logout, review exit or main-window refresh), it clears the rendered document and URL. The main host clears state on destroy. Popup blocking is reported to the user while preserving the fallback link.
- Retain PDF.js 4.10.38 for this change. The maintained upstream release is newer, but the current 6.1.200 package requires Node >=22.13 while this repository's installed tooling is Node-20-compatible. Upgrade PDF.js separately after a browser/build compatibility and security review; do not couple that breaking dependency jump to the viewer rollout.
Consequences¶
- Study navigation, API authorization and existing PDF URL generation are not changed.
- The feature can be enabled gradually through generated configuration, but defaults false in code and Helm values.
- A popup has deliberately narrower resilience than a full second application tab: it cannot retain a document after the coordinating main window disappears.
- PDF.js rendering stays in a SyRF page, avoiding an untrusted third-party viewer wrapper. The direct fallback remains available for browser accessibility tooling and failed popups.
- A server-side PDF proxy is deliberately out of scope: it would need separate authorization and threat modelling for SSRF, redirects, credentials, byte ranges and revocation behaviour.
Alternatives considered¶
- BroadcastChannel-only coordination: rejected because any same-origin participant can hear broadcast state and it does not bind a viewer to the opener that created it.
- PDF URL/study route parameters: rejected because history, refresh and copied URLs would retain sensitive, potentially expired authority outside the main review lifecycle.
- Iframe/native browser rendering in the main form: rejected for this desktop-first slice; it competes with review width and does not serve multi-monitor reviewers as well.
- Upgrade to newest PDF.js now: rejected as an unrelated Node/toolchain compatibility change.
Rollout and validation plan¶
- Keep the generated flag false everywhere and validate popup-blocked, unavailable, stale-load, close/reopen, rapid-navigation and logout/review-exit behaviour with focused automated tests.
- Enable only in a non-production environment after manual multi-monitor and keyboard/screen reader checks against managed, external and expired URLs.
- Obtain explicit authorization for a narrowly scoped production cohort; monitor popup failures, PDF load failures and navigation misalignment before widening. No rollout is implied by this ADR or PR.
References¶
- PDF.js releases
- PDF.js production setup guidance
src/libs/project-management/SyRF.ProjectManagement.Core/Services/PdfLinkBuilder.cssrc/services/web/src/app/pdf-tools/pdf-loader.service.ts