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
SlotReservationdomain 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:
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:
- Comfort: acknowledge without blame. Passive system voice.
- Explain: one short sentence about why. No jargon.
- 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¶
-
Terminology: "review slot" — more specific than "spot" alone, clear in context, appropriately conversational for an academic tool.
-
#9 incident disclosure: no bug framing. Friendly save-regularly advice. Sentry logging is internal only.
-
#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.
-
#1 false promise: "start and save" not "start to keep". Only saving secures the spot permanently.