Skip to content

Session & Reservation Copy Review

A review of all reviewer-facing copy related to study slots, disconnection, and capacity. Proposes improved language based on UX writing research and domain-specific terminology.

Terminology update (2026-05-03): this document has been revised so the reviewer-facing term is "review slot" throughout (previously "review spot"). This aligns the user-visible copy with the internal SlotReservation domain class and the active-reviewer-tracking spec for PR #2467.

Research findings

String externalisation (Angular best practice)

Angular's built-in @angular/localize is heavyweight (separate builds per locale) and overkill for a monolingual app. ngx-translate adds a runtime dependency and JSON file management. The pragmatic middle ground for "no i18n now, ready later" is typed TypeScript constants files, one per feature area:

export const SESSION_MESSAGES = {
  idle: { title: '...', body: '...' },
} as const;

All copy reviewable in one place, type-safe, and trivially migratable to $localize tagged templates later.

Recommendation: create src/app/study-presence/session-messages.ts.

UX writing for session/status messages

Research into collaborative tools (Google Docs, Figma, Notion) surfaces a consistent Comfort - Explain - Act pattern:

  1. Comfort: acknowledge without blame. Passive system voice.
  2. Explain: one short sentence about why. No jargon.
  3. Act: a clear, single next step.

Key principles: - Never blame the user ("you were idle too long" -> "this study has been released") - Be honest about what's at risk - but only mention what's actually at risk - Use concrete domain language, not system-speak

Important context: when each message can appear

These banners/overlays only appear for reviewers who have NOT yet saved annotations on this study. The code confirms this:

StageReviewService.EnsureActiveReviewSessionAsync (line 369-378) explicitly skips creating an ActiveReviewSession if the reviewer already has a saved AnnotationSession for this stage. No ActiveReviewSession means no idle timer, no suspended timer, no disconnection tracking, and no banners.

This means: - Idle banner: reviewer opened the study but hasn't typed anything - Disconnection banner: reviewer was working but hadn't saved yet - Surplus banner/lock: reviewer lost their spot before saving - Save guard rejection: reviewer typed annotations that were NOT saved

There is no scenario where "your previously saved work is safe" is relevant for these messages. The reviewer either has no work to lose (idle) or has unsaved form data that IS at risk (disconnect/rejection). Saying "saved work is safe" is misleading. The proposed copy below is corrected accordingly.

Domain language

The meaningful concept for reviewers is:

Your review slot on this study - the right to review and submit annotations. Each study needs a set number of reviews. When you open a study, you're given a review slot. Saving your annotations secures your spot permanently. Until you save, your spot is temporary and may be released if you're inactive or lose your connection.

Internal term Reviewer-facing term
ActiveReviewSession "your review slot"
AnnotationSession "your saved review" / "your review"
SessionCountTarget "the number of reviews needed"
TotalAllocatedSessionCount (never exposed to reviewers)
Suspended / Expired "offline" / "released"
Surplus "all review slots filled" / "enough reviewers"

Current copy vs proposed improvements

1. Idle banner (form clean, 5+ min no interaction)

The reviewer opened the study, was given a review slot, but hasn't interacted with the annotation form. There is nothing to lose — they haven't typed anything. This banner only shows when HasStartedAnnotating = false.

Component: idle-notification-banner (active state)

Text
Before Your session is inactive — You can pick up where you left off — just continue working on the annotation form below. If you stay away, another reviewer may take your place in [countdown].
After You haven't started reviewing yet — Your review slot on this study is still yours, but if you don't begin, it will be given to another reviewer in [countdown]. To secure your spot, start and save your annotations.

Changes: "session is inactive" -> "you haven't started reviewing yet" (specific, honest). No false promise that starting alone keeps the spot — the copy now says "start and save" to set correct expectations. The reviewer knows that saving is what secures their spot permanently.

2. Idle session expired (timer ran out, review slot removed)

The reviewer never interacted and the idle timeout removed their review slot. Nothing was lost — they never started.

Component: idle-notification-banner (expired state)

Text
Before Your reserved place on this study has expired — You can still start reviewing, or skip to your next study.
After Your review slot on this study has been released — You didn't start reviewing, so your spot was given to another reviewer. You can move on to your next study.

Changes: "reserved place has expired" -> "review slot released" (concrete). Removed "you can still start reviewing" (contradicts the release — they can't re-join if at capacity). Honest about why: "you didn't start reviewing".

3. Surplus warning banner (other reviewers filled remaining spots)

The reviewer was on the study but other reviewers filled all review slots. The reviewer hasn't saved, so they haven't committed to a slot.

Component: surplus-warning-banner

Text
Before This study now has enough reviewers — While you were away, other reviewers picked up this study and it now has the reviews it needs. You can move on to the next study that needs your attention.
After This study now has all the reviewers it needs — While you were away, other reviewers filled the remaining review slots. You can move on to your next study.

Changes: "enough reviewers" -> "all the reviewers it needs". Shortened.

4. Surplus notification dialog (shown once on entering surplus state)

Same scenario as #3, but as a one-time dialog.

Component: surplus-notification-dialog

Text
Before This study now has enough reviewers — While your session was inactive, other reviewers picked up this study and it now has the reviews it needs. You can move on to the next study that needs your attention.
After This study now has all the reviewers it needs — Other reviewers filled the remaining review slots while you were away. You can close this and move on to your next study.

Changes: "session was inactive" -> "while you were away".

5. Surplus form lock overlay (form disabled, all spots taken + enforcement on)

The reviewer can't submit because all review slots are filled and enforcement is enabled by the project admin.

Component: stage-review.component.html (surplus lock overlay)

Text
Before Annotation form locked — This study has reached its annotation target and enforcement is enabled. You cannot submit annotations for this study.
After This study already has all the reviews it needs — All review slots on this study are filled. Please move on to your next study.

Changes: "annotation form locked" and "enforcement is enabled" are cold system language that exposes admin configuration. The reviewer doesn't need to know about enforcement settings.

6. Disconnection countdown banner (offline 10+ min, within grace)

The reviewer lost their connection while they had a review slot. They may or may not have started typing. Their spot is held for 2 hours by the server.

Component: disconnection-banner (countdown state)

Text
Before You're disconnected from the server — Don't worry — your place on this study is being held for you. If your connection comes back, you'll pick up right where you left off. If it doesn't reconnect within [countdown], your place will be released so another reviewer can continue.
After You're currently offline — Your review slot on this study is being held for you. When your connection returns, you'll be able to continue. If you stay offline, your review slot will be released in [countdown] so another reviewer can take over. If you've started filling in the form, any unsaved answers will be lost — we recommend saving your progress regularly.

Changes: "disconnected from the server" -> "offline" (simpler). "Don't worry" removed (can feel patronising). Honest about unsaved form data being at risk. Friendly nudge to save regularly.

7. Disconnection expired banner (offline 2h+, grace elapsed)

The client-side timer has elapsed. The reviewer is still disconnected — we don't yet know whether another reviewer took the spot. The server removed the review slot after 2h, but another spot may still be available.

Component: disconnection-banner (expired state)

Text
Before Your reservation on this study has expired — You were disconnected for too long and your place has been released. Once your connection is restored, we'll automatically check whether this study is still available for you to review.
After Your review slot on this study has been released — You've been offline for an extended period, so your review slot is no longer being held. When your connection returns, we'll check whether a review slot is still available for you.

Changes: "reservation" -> "review slot". Removed false certainty about "given to another reviewer" — we don't know that yet (still disconnected). Says "no longer being held" (factual) rather than "given to someone else" (unknown). The actual outcome is discovered on reconnection.

8. Reservation expired form lock overlay (form disabled while offline past grace)

Same scenario as #7 but shown in the form area itself.

Component: stage-review.component.html (reservation expired overlay)

Text
Before Form unavailable — You were disconnected for too long and your reservation on this study has expired. Once your connection is restored, we'll check whether this study is still available for you to review.
After Your review slot on this study has been released — You've been offline for an extended period, so your review slot is no longer being held. When your connection returns, we'll check whether a review slot is still available.

Changes: "Form unavailable" is vague. Same language as #7 for consistency.

9. Atomic save guard rejection (last-resort server rejection)

The reviewer managed to click Save but the server rejected it because their review slot was gone and the study is at capacity. This should not normally happen — it means all upstream guards (client-side form lock, reconnection handling) were bypassed. Annotations were NOT saved.

Component: ReviewController (server response) + client error handling

Text
Before "Study {studyId} has reached the target annotation capacity for stage {stageId}. Your reservation may have expired. Please navigate to a different study."
After We couldn't save your review — Your review slot on this study was released while you were working, and all review slots are now filled. The annotations you just entered were not saved. For future reviews, we recommend saving your progress regularly so your work isn't lost if your session ends unexpectedly. Please try another study.

Changes: completely rewritten. No GUIDs or system jargon. Honest that annotations were NOT saved (critical to say explicitly). Friendly, forward-looking advice about saving regularly rather than framing it as a system bug.

Server-side action: this scenario must still be reported to Sentry with full context (studyId, stageId, investigatorId, stored tally at the time of rejection) since it indicates a gap in the upstream prevention chain worth investigating — but this is internal, not surfaced to the reviewer.

Summary of changes

Change Rationale
Consistent "review slot" language Domain-appropriate, concrete, avoids ambiguity of "spot" alone
Remove all "saved work is safe" claims These banners only show for reviewers who haven't saved — the claim is false
"Start and save" not "start to keep" Starting alone doesn't secure the spot — only saving does. No false promises
Honest about unsaved form data risk Disconnect scenarios may lose typed-but-unsaved answers
#7/#8: no false certainty about outcome Client doesn't know if another reviewer took the spot while still offline
#9: friendly advice, not bug disclosure Encourage saving regularly rather than alarming the reviewer
Remove system jargon "session", "reservation", "enforcement", "capacity", "target" are not reviewer concepts
Comfort - Explain - Act structure Proven pattern from collaborative tool UX research
Centralise in session-messages.ts Reviewable, type-safe, i18n-ready
Sentry reporting for #9 (internal only) Defense-in-depth failure needs investigation, but not surfaced to reviewer

Design note: no dirty-form timeout

Currently, once a reviewer starts typing (HasStartedAnnotating = true), the 5-minute idle timer is cancelled and no replacement timer is scheduled. The session stays alive indefinitely as long as the SignalR connection is open.

This means a reviewer who types one character and walks away holds their spot forever — their laptop stays on, the browser tab stays open, heartbeats keep flowing, and the study is blocked. The only way the spot is freed is if their connection drops (laptop sleep, wifi loss → suspended → 2h → removed).

This is a gap worth addressing in a future PR:

  • Option A: schedule a longer "dirty idle" timer (e.g. 4h) after which the session is flagged for admin attention but not automatically removed.
  • Option B: add admin visibility into long-running dirty sessions so admins can contact the reviewer directly.

For this PR, the domain model should capture timestamps that enable future admin tooling (see "Lifecycle timestamps" section below).

Lifecycle timestamps (to add in this PR)

The domain model should record key moments in the review lifecycle to support future admin dashboards and operational visibility.

ActiveReviewSession (transient — removed when reviewer leaves/times out):

Field Set when Purpose
JoinedAtUtc Construction (JoinStudyReview) When the reviewer opened the study
StartedAnnotatingAtUtc MarkDirty() first called When the form first became dirty

These transfer to ExpiredReviewSession when the session times out, giving admins a full picture: "reviewer X opened study Y at 10:00, started editing at 10:05, was removed at 12:05 after going offline."

AnnotationSession (permanent — persists after save):

Field Set when Purpose
CreatedAtUtc Construction (first save) When the reviewer first saved
CompletedAtUtc UpdateStatus(Completed) When the reviewer marked the review complete

These give admins insight into review duration and can power future analytics (e.g. "average time from first save to completion").

Resolved decisions

  1. Terminology: "review slot" — more specific than "spot" alone, clear in context, appropriately conversational for an academic tool.

  2. #9 incident disclosure: no bug framing. Friendly save-regularly advice. Sentry logging is internal only.

  3. #7/#8 certainty: don't claim another reviewer took the spot while still offline. Say "no longer being held" and promise to check on reconnect.

  4. #1 false promise: "start and save" not "start to keep". Only saving secures the spot permanently.