Engineering articles
← All articles

Restoring DICOM answers in an embedded viewer

Telegnost LMS: putting a learner's saved measurements back after a reload.

In short

Some exam answers are measurements on a medical image. After a reload they come back for the same question and attempt, and nowhere else.

From Telegnost LMS · Technical lead

Architecture

  1. React learning and assessment UI
  2. Typed service registry and TanStack Query
  3. Moodle functions and custom plugins
  4. DICOM assessment viewer

What was hard

In a DICOM exam question, the learner answers by taking measurements on a medical image inside an embedded viewer. Navigating away and back, or reloading, creates the viewer iframe afresh, and the learner's saved measurements have to come back into it: for the right question, the right attempt, and nobody else's.

The LMS page and the viewer are separate windows that only talk through postMessage, so each side has to establish who sent a message before acting on it.

How I found it

Client acceptance testing set the requirement: measurements must survive navigation and reloads. I reproduced each case in the browser. The viewer had no recovery at all, so this was built from scratch on both sides.

What shipped

The viewer sends STUDENT_READY when it loads. The LMS page finds the iframe whose contentWindow matches event.source, checks event.origin against that iframe's URL, and checks that the question ID and attempt ID, in the message and in the iframe URL, match the current attempt.

Only then does it read the saved answer and post STUDENT_RESTORE to that viewer's origin. It looks for the answer textarea in the assessment container first and falls back to the essay answer in the question HTML; a parser turns the stored text back into measurements.

The viewer runs the matching checks on its side before it restores anything: a real origin, the parent window as sender, the right question and attempt, and an answer that is a list. Later rounds kept drafts in the browser session when the viewer runs outside the LMS, made Undo reach the LMS autosave, and added freehand outlines.

Code excerptstudent-dicom-recovery.ts, studentRecovery.js
// LMS page: answer the viewer that asked, for this attempt only.
for (const iframe of container.querySelectorAll("iframe")) {
  if (iframe.contentWindow !== event.source) continue;
  const url = new URL(iframe.src);
  const questionId = url.searchParams.get("questionId");
  if (event.origin !== url.origin ||
      event.data.payload?.questionId !== questionId ||
      String(event.data.payload?.attemptId) !== String(attemptId) ||
      url.searchParams.get("attemptId") !== String(attemptId))
    return false;
  // ...read the saved answer, parse it into measurements...
  iframe.contentWindow?.postMessage({ type: "STUDENT_RESTORE",
    payload: { questionId, attemptId: String(attemptId),
      studentAnswer: measurements } }, url.origin);
  return true;
}

// Viewer: restore only what its own parent sent for this attempt.
export function acceptsStudentRestore(event, context, parentWindow) {
  return context.origin !== '*' && event.source === parentWindow &&
    event.origin === context.origin &&
    event.data?.type === 'STUDENT_RESTORE' &&
    event.data.payload?.questionId === context.questionId &&
    String(event.data.payload?.attemptId) === context.attemptId &&
    Array.isArray(event.data.payload?.studentAnswer);
}

Condensed from both sides of the handshake.

Result

In the live test on 27 September 2026, a separate signed-in browser session recovered the learner's annotation and radio answer.

Contact Abdulrhman

Matching Spotify recordings to Discogs credits

Browse all articles