Skip to content

feat(escape-room): ship generalized quiz mode - #5143

Open
rschlaefli wants to merge 77 commits into
v3from
escape-room-quiz-mode-plan
Open

feat(escape-room): ship generalized quiz mode#5143
rschlaefli wants to merge 77 commits into
v3from
escape-room-quiz-mode-plan

Conversation

@rschlaefli

@rschlaefli rschlaefli commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

This PR ships Escape Room mode across Practice Quizzes, Microlearnings, Group Activities, and Live Quiz blocks. Participants clear stages in order under a server-controlled timer, can request hints for a time penalty, and receive an enforced lockout after wrong answers. Lecturers can author the mode, monitor progress, and reset attempts.

It also adds the QR Scan element, including owner-only code access, printable decoys, camera scanning, and a validated manual fallback.

The full execution and review record is in the production roadmap.

What changed

  • Added a canonical Escape Room lifecycle for start, progress, expiry, completion, hints, and reset.
  • Enforced participant, course, group, block, and current-stage scope before grading or revealing content.
  • Serialized concurrent submissions with attempt and stage claims, then rechecked lifecycle state after claim acquisition.
  • Rejected Live Quiz responses after the active block closes, including late and retry races.
  • Added server-anchored timers, reload-safe hints, retry and lockout flows, and shared group attempts.
  • Added lecturer authoring, progress rosters, polling, permission-aware monitoring, and reset controls.
  • Added QR Scan contracts, migration, authoring, print sheets, decoys, grading, camera handling, and manual entry.
  • Preserved Escape Room configuration, hints, and QR elements through Live Quiz templates and editing.
  • Updated the engineering wiki, lecturer tutorial, student tutorial, testing guidance, skills, and release evidence.
  • Integrated the current v3 baseline without changing Escape Room behavior.

Security and data integrity

  • Client input cannot grant owner or preview authority.
  • Locked stages, unused hints, and QR answer codes stay out of participant payloads.
  • Group submissions require the exact answerable instance set and commit atomically.
  • Hint charging and response grading are single-winner under concurrency.
  • Reset, progress, print, and edit paths enforce their actual READ, WRITE, or owner permissions.
  • Attempt statistics have one owner and pruning is retention-aware and retry-safe.
  • The final security review found no Critical or Important issue.

Branch coverage

  • Base: v3 at c8de9c897
  • Head: 4be19aa61
  • Reviewed: 77 commits; 214 files changed, 21,211 insertions, and 1,624 deletions against v3
  • Covered: the generalized Escape Room lifecycle, participant and lecturer flows across all four activity modes, QR Scan, schema and generated GraphQL artifacts, migrations, tests, tutorials, engineering guidance, runtime evidence, review cleanup, and plan progress
  • The final base sync applied the sole new v3 delta exactly. Its upstream and merge patches share stable patch-id 11faeca2f97673cd2bc0d6f825b12e430b71b802, and it touched no Escape Room behavior path.

Review focus

  • Check the server-side stage, participant, activity, and lifecycle gates around grading, hints, reset, and late Live Quiz responses.
  • Check concurrency ownership for group submissions, hint charging, completion, and response processing.
  • Check the QR Scan owner/participant data boundary, concurrent index migration, and manual fallback.
  • Treat generated GraphQL artifacts and plan progress as supporting output; the main behavior lives in the GraphQL services, Response API, PWA, Manage UI, and shared components.

Verification

Current head:

  • pnpm run check:all in the exact Node 24 DevPod: 24/24 tasks pass, including Prisma parity, formatting, lint, syncpack, and agent-doc checks
  • Production build in the same DevPod: 21/21 tasks pass
  • git diff --check: pass
  • GitHub checks for 4be19aa61: 49/49 pass, including all eight Playwright shards

Earlier branch verification, still applicable because the final base sync and plan commits touched no Escape Room behavior path:

  • GraphQL Escape Room tests: 86/86 pass
  • Response API tests: 20/20 pass
  • Routed Playwright Escape Room suite: 19/19 pass in 2.4 minutes
  • Prisma migration replay and schema parity: 179 migrations replayed; the concurrent QR index is valid
  • Docusaurus production build: pass
  • OKF core validation: pass with 20 existing hygiene warnings
  • Diff-aware Opengrep against v3: 0 findings
  • Final security, Klicker branch, and thermonuclear reviews: approve with no Critical or Important findings
  • GitHub review threads: 43/43 resolved

Failed or warning:

  • A raw package-wide tsc --noEmit probe for @klicker-uzh/prisma-data still reports historical errors under src/scripts/*. The package exposes no configured check for that probe, none of the synchronized seed files appear in the errors, and the repository's configured typecheck passes.
  • A physical camera was unavailable for the final local run.

Not run:

  • Local Playwright was not repeated after the base sync because the synchronized patch and final plan commits changed no Escape Room behavior. The 49 passing GitHub checks on the current head provide the fresh runtime gate, including all eight Playwright shards.

The earlier Playwright run used the exact codex-escape-room-production DevPod and namespaced devrouter routes. It covers Practice Quiz, Microlearning, QR fallback, two concurrent Group Activity participants with lecturer monitoring and reset, and Live Quiz participant, cockpit, reset, and reload behavior.

Runtime evidence

Group dashboard, English desktop Group participant, German mobile
Group dashboard Group participant
Live Quiz cockpit, German desktop Live Quiz participant, English mobile
Live Quiz cockpit Live Quiz participant
QR print sheet, English desktop QR manual fallback, German mobile
QR print sheet QR manual fallback

Known limitation

Camera denial and the complete manual-entry fallback are browser-verified. Camera acquisition, cleanup, validation, and grading have automated coverage, but the final local run did not use a physical camera.

Blocking before merge

  • A reviewer must approve the PR.
  • Merge requires explicit user authority.

Follow-up after merge

None.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Escape Room mode and QR Scan elements are added across persistence, GraphQL, response processing, management forms, participant applications, lecturer evaluation, scheduled pruning, documentation, and automated tests. The implementation includes timed attempts, sequential gating, hints, lockouts, QR grading, reset controls, and progress reporting.

Changes

Escape Room and QR Scan Platform

Layer / File(s) Summary
Persistence and shared contracts
apps/analytics/prisma/schema/*, packages/prisma/src/prisma/schema/*, packages/types/*, packages/util/*
Adds QR scan types and codes, Escape Room configuration and attempt models, shared validation utilities, hint storage rules, and migrations.
QR scan authoring and grading
apps/frontend-manage/src/components/elements/*, packages/graphql/src/services/elements.ts, packages/graphql/src/services/stacks.ts, packages/shared-components/src/*
Adds QR scan creation, editing, printing, scanning, normalization, validation, and grading.
Escape Room management workflows
apps/frontend-manage/src/components/activities/*, apps/frontend-manage/src/pages/index.tsx
Adds Escape Room settings, validation, hint authoring, QR-compatible stack editing, form submission, and layout adjustments.
GraphQL lifecycle and enforcement
packages/graphql/src/schema/*, packages/graphql/src/services/*
Exposes Escape Room configuration, attempts, hints, progress, and reset operations while enforcing participant access, timing, sequential progression, lockouts, and completion.
Participant Escape Room experience
apps/frontend-pwa/src/components/*, apps/frontend-pwa/src/pages/*
Adds start/completion overlays, countdowns, hint requests, lockout handling, response scoping, sequential navigation, and activity-specific Escape Room integration.
Live response enforcement and deduplication
apps/response-api/src/*, apps/hatchet-worker-response-processor/src/*
Adds authenticated LiveQuiz validation, QR grading, Redis attempt tracking, event deduplication, retry handling, and Redis transaction compatibility checks.
Evaluation and operations
apps/frontend-manage/src/components/evaluation/*, packages/graphql/src/services/pruneEscapeRooms.ts, packages/hatchet/*
Adds lecturer progress dashboards, reset controls, scheduled attempt pruning, Hatchet task contracts, and operational wiring.
Integration validation and E2E coverage
packages/graphql/test/*, apps/response-api/src/escapeRoom.test.ts, playwright/tests/Z-escape-room.spec.ts
Adds coverage for authorization, timing, hints, QR scans, lockouts, retries, completion, reset behavior, pruning, progress aggregation, and browser workflows.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

Suggested labels: dependencies

Suggested reviewers: jabbadizzlecode

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately describes the main escape-room mode change.
Description check ✅ Passed The description is thorough and aligned with the PR; it only misses the template's ClickUp link/checklist formatting.

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.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown

Confidence Score: 3/5

The core escape-room mechanics work, but the hint system claimed in the PR description is completely unimplemented, leaving penaltySeconds, hintsUsed, and hintPenalty as dormant schema columns with no write path.

The sequential gating, timer, lockout, and completion logic across stacks.ts, groups.ts, and escapeRoom.ts are sound. The migration correctly creates all four unique indices. However, requestEscapeRoomHint — one of three mutations the PR explicitly claims to ship — has no implementation anywhere in the codebase. Participants can see the Hint Penalty UI but cannot request hints, and penaltySeconds is permanently 0.

packages/graphql/src/services/practiceQuizzes.ts needs the missing hint mutation; apps/response-api/src/escapeRoom.ts has the cookie-parsing bug; packages/graphql/src/schema/query.ts needs elementBlockId added to the progress query.

Important Files Changed

Filename Overview
apps/response-api/src/escapeRoom.ts New handler for live-quiz escape-room validation; auto-creates attempts and grades responses. Contains a cookie-parsing bug where values are truncated at the first = sign.
packages/graphql/src/services/practiceQuizzes.ts Adds startEscapeRoomAttempt and resetEscapeRoomAttempt (lecturer-only); the advertised third mutation requestEscapeRoomHint is entirely absent, leaving penaltySeconds and hintsUsed as dead schema columns.
packages/graphql/src/services/stacks.ts Adds sequential gating, timer expiry, lockout, and completion marking to respondToElementStack for escape-room activities. Logic is sound for question-only stacks.
packages/graphql/src/services/escapeRooms.ts New lecturer-facing progress service; correctly computes cleared stacks from QuestionResponse data and maps attempt state. Authorization deferred to resolver withPermission.
packages/graphql/src/services/groups.ts Extended submitGroupActivityDecisions to validate escape-room attempt state, check correctness of all responses, and apply lockout or mark completion. Content types are correctly skipped in the correctness loop.
packages/graphql/src/schema/query.ts Adds escapeRoomProgress query with withPermission guard; elementBlockId arg is missing, making live-quiz progress inaccessible via GraphQL.
packages/prisma/src/prisma/schema/migrations/20260707103049_add_escape_room_mode/migration.sql Correctly adds EscapeRoomConfig and EscapeRoomAttempt tables with all four expected unique indices including participantId_elementBlockId.
packages/graphql/src/services/pruneEscapeRooms.ts New Hatchet job that aggregates per-instance statistics for finished attempts with an idempotency marker, then prunes stale completed/expired records after 90 days.
apps/frontend-pwa/src/components/hooks/useEscapeRoom.ts Client-side escape room state hook managing countdown, start/reset mutations, and derived status flags. Correctly handles penalty in deadline calculation.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant P as Participant (PWA)
    participant GQL as GraphQL API
    participant RA as Response API
    participant DB as Postgres

    P->>GQL: startEscapeRoomAttempt(activityId)
    GQL->>DB: Check course enrollment
    GQL->>DB: Create EscapeRoomAttempt (IN_PROGRESS)
    GQL-->>P: "attempt { startedAt, timeLimit, penaltySeconds=0 }"

    loop Each stack (sequential)
        P->>GQL: respondToElementStack(stackId, responses)
        GQL->>DB: Load attempt, check status / lockout / elapsed
        GQL->>DB: Check preceding stacks all CORRECT (gating)
        GQL->>DB: Save QuestionResponse
        alt All correct
            GQL->>DB: Update attempt COMPLETED
            GQL-->>P: "stackFeedback=CORRECT"
        else Incorrect
            GQL->>DB: Set lockoutUntil
            GQL-->>P: GraphQLError locked out
        else Expired
            GQL->>DB: Update attempt EXPIRED
            GQL-->>P: GraphQLError expired
        end
    end

    Note over P,GQL: Live Quiz path uses Response API
    P->>RA: POST /response (cookie auth)
    RA->>DB: Find/create EscapeRoomAttempt
    RA->>DB: Grade response, apply lockout if incorrect
    RA-->>P: status correct or incorrect

    Note over GQL: Lecturer dashboard
    P->>GQL: escapeRoomProgress(activityId)
    GQL->>DB: Load config, stacks, attempts, QuestionResponses
    GQL-->>P: EscapeRoomProgress attempts totalStacks
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant P as Participant (PWA)
    participant GQL as GraphQL API
    participant RA as Response API
    participant DB as Postgres

    P->>GQL: startEscapeRoomAttempt(activityId)
    GQL->>DB: Check course enrollment
    GQL->>DB: Create EscapeRoomAttempt (IN_PROGRESS)
    GQL-->>P: "attempt { startedAt, timeLimit, penaltySeconds=0 }"

    loop Each stack (sequential)
        P->>GQL: respondToElementStack(stackId, responses)
        GQL->>DB: Load attempt, check status / lockout / elapsed
        GQL->>DB: Check preceding stacks all CORRECT (gating)
        GQL->>DB: Save QuestionResponse
        alt All correct
            GQL->>DB: Update attempt COMPLETED
            GQL-->>P: "stackFeedback=CORRECT"
        else Incorrect
            GQL->>DB: Set lockoutUntil
            GQL-->>P: GraphQLError locked out
        else Expired
            GQL->>DB: Update attempt EXPIRED
            GQL-->>P: GraphQLError expired
        end
    end

    Note over P,GQL: Live Quiz path uses Response API
    P->>RA: POST /response (cookie auth)
    RA->>DB: Find/create EscapeRoomAttempt
    RA->>DB: Grade response, apply lockout if incorrect
    RA-->>P: status correct or incorrect

    Note over GQL: Lecturer dashboard
    P->>GQL: escapeRoomProgress(activityId)
    GQL->>DB: Load config, stacks, attempts, QuestionResponses
    GQL-->>P: EscapeRoomProgress attempts totalStacks
Loading

Reviews (10): Last reviewed commit: "test(graphql): seed instanceStatistics i..." | Re-trigger Greptile

@gitguardian

gitguardian Bot commented Jul 8, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@rschlaefli
rschlaefli marked this pull request as ready for review July 8, 2026 15:12
Copilot AI review requested due to automatic review settings July 8, 2026 15:12
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 8, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@dosubot dosubot Bot added the feature label Jul 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread playwright/tests/Z-escape-room.spec.ts Fixed
Comment thread playwright/tests/Z-escape-room.spec.ts Fixed
Comment thread packages/graphql/src/services/practiceQuizzes.ts Fixed
Comment thread packages/graphql/src/services/practiceQuizzes.ts Outdated
@rschlaefli rschlaefli changed the title docs(project): escape room quiz mode implementation plan feat(quiz): generalized escape room mode and response validation Jul 8, 2026
Comment thread packages/graphql/src/services/practiceQuizzes.ts Outdated
Comment thread packages/graphql/src/services/practiceQuizzes.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/graphql/src/services/groups.ts (1)

1508-1720: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the attempt transition atomic.

Concurrent group members can both pass the IN_PROGRESS check, then race to overwrite decisions and set completion/lockout state. Persist answers, decisions, and a conditional attempt-state transition in one transaction so only one submission wins.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/groups.ts` around lines 1508 - 1720, Refactor
the escape-room submission flow around the answer persistence block,
`updatedActivityInstance` update, and `escapeRoomAttempt` updates to use one
Prisma transaction. Within that transaction, persist all answer results and
decisions, then conditionally transition the attempt from `IN_PROGRESS` using an
updateMany or equivalent guarded update so concurrent submissions cannot both
win; derive completion or lockout behavior from the conditional update count and
roll back all changes when the submission is rejected.
🟠 Major comments (30)
packages/graphql/src/schema/groupActivity.ts-18-18 (1)

18-18: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use a path alias for the configuration import.

As per coding guidelines, “Use @ and ~ path aliases for imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/groupActivity.ts` at line 18, Replace the
relative EscapeRoomConfigRef import in groupActivity.ts with the configured @ or
~ path alias, matching the project’s existing alias convention for this module.

Source: Coding guidelines

packages/graphql/src/index.ts-13-13 (1)

13-13: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use configured path aliases for the new imports.

Replace these relative module paths with the corresponding @ or ~ aliases.

As per coding guidelines, “Use @ and ~ path aliases for imports.”

Also applies to: 63-63

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/index.ts` at line 13, Update the new imports in the
GraphQL entry module, including the schema/escapeRoomConfig import and the other
affected import, to use the project’s configured @ or ~ path aliases instead of
relative module paths.

Source: Coding guidelines

packages/graphql/src/schema/escapeRoomConfig.ts-2-2 (1)

2-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use a path alias for the builder import.

As per coding guidelines, “Use @ and ~ path aliases for imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/escapeRoomConfig.ts` at line 2, Replace the
relative builder import in the escape room configuration module with the
configured @ or ~ path alias, preserving the existing builder symbol and import
behavior.

Source: Coding guidelines

packages/graphql/src/schema/practiceQuiz.ts-18-18 (1)

18-18: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use a path alias for the configuration import.

As per coding guidelines, “Use @ and ~ path aliases for imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/practiceQuiz.ts` at line 18, Update the import in
the practice quiz schema to use the configured @ or ~ path alias instead of the
relative './escapeRoomConfig.js' path, while preserving the EscapeRoomConfigRef
symbol.

Source: Coding guidelines

packages/graphql/src/schema/liveQuiz.ts-5-6 (1)

5-6: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use path aliases for the new schema imports.

As per coding guidelines, “Use @ and ~ path aliases for imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/liveQuiz.ts` around lines 5 - 6, Update the
imports in the live quiz schema to use the project’s configured @ or ~ path
aliases instead of relative paths, including EscapeRoomConfigRef,
EscapeRoomAttemptRef, and PublicationStatus.

Source: Coding guidelines

apps/analytics/prisma/schema/quiz.prisma-489-499 (1)

489-499: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Enforce one escape-room target per row at apps/analytics/prisma/schema/quiz.prisma:482-536. EscapeRoomConfig and EscapeRoomAttempt still allow multiple nullable relation columns to be set together, or none at all, so a row can point at conflicting activities or the wrong participant/group scope. The migration only adds unique indexes and foreign keys; add SQL CHECK constraints for exactly one config target and one valid attempt target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/analytics/prisma/schema/quiz.prisma` around lines 489 - 499, The
EscapeRoomConfig and EscapeRoomAttempt models allow zero or multiple nullable
targets. Add migration SQL CHECK constraints enforcing exactly one non-null
target per config row and exactly one valid target per attempt row, including
participant/group scope consistency as required by the existing relations.
Update the Prisma migration rather than relying on the schema’s unique indexes
and foreign keys alone.
apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx-134-152 (1)

134-152: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Same hardcoded validation messages as MicroLearningWizard.

Apply the same i18n fix suggested for MicroLearningWizard.tsx lines 140-158.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx`
around lines 134 - 152, Replace the hardcoded validation messages in the
PracticeQuizWizard validation schema for escapeRoomTimeLimit and
escapeRoomHintPenalty with the same i18n translation approach used by
MicroLearningWizard, including required, integer, positive, and minimum-value
messages.
apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizSettingsStep.tsx-166-206 (1)

166-206: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Same hardcoded English strings as MicroLearningSettingsStep.

The escape room labels here are also hardcoded instead of using t(). Apply the same i18n fix as suggested for MicroLearningSettingsStep.tsx.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizSettingsStep.tsx`
around lines 166 - 206, Replace the hardcoded “Escape Room Mode”, “Time Limit
(minutes)”, and “Hint Penalty (seconds)” labels in the PracticeQuizSettingsStep
escape-room section with translated strings using the existing t() i18n pattern
from MicroLearningSettingsStep. Preserve the current field behavior and required
configuration.
apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningSettingsStep.tsx-165-202 (1)

165-202: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use i18n translations instead of hardcoded English strings.

The escape room labels ("Escape Room Mode", "Time Limit (minutes)", "Hint Penalty (seconds)") are hardcoded while the rest of the component uses t() from useTranslations(). This breaks localization for German users. The same pattern appears in PracticeQuizSettingsStep.tsx.

🌐 Proposed fix
-                    <Checkbox
-                      label="Escape Room Mode"
+                    <Checkbox
+                      label={t('manage.activityWizard.escapeRoomMode')}
                       checked={!!values.isEscapeRoom}
-                        <FormikNumberField
-                          name="escapeRoomTimeLimit"
-                          label="Time Limit (minutes)"
+                        <FormikNumberField
+                          name="escapeRoomTimeLimit"
+                          label={t('manage.activityWizard.escapeRoomTimeLimit')}
                           required
-                        <FormikNumberField
-                          name="escapeRoomHintPenalty"
-                          label="Hint Penalty (seconds)"
+                        <FormikNumberField
+                          name="escapeRoomHintPenalty"
+                          label={t('manage.activityWizard.escapeRoomHintPenalty')}
                           required
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningSettingsStep.tsx`
around lines 165 - 202, Replace the hardcoded escape-room labels in the relevant
fields within MicroLearningSettingsStep, and apply the same change in
PracticeQuizSettingsStep, by using the component’s existing useTranslations()
hook and appropriate translation keys for “Escape Room Mode,” “Time Limit
(minutes),” and “Hint Penalty (seconds).”
apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningWizard.tsx-140-158 (1)

140-158: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use i18n translations for validation messages.

The escape room validation messages ("Time limit is required", "Must be an integer", etc.) are hardcoded English strings. The same pattern appears in PracticeQuizWizard.tsx. Use t() for consistency with the rest of the schema.

🌐 Proposed fix
     isEscapeRoom: yup.boolean(),
     escapeRoomTimeLimit: yup.number().when('isEscapeRoom', {
       is: true,
       then: (schema) =>
         schema
-          .required('Time limit is required')
-          .integer('Must be an integer')
-          .positive('Must be a positive number of minutes'),
+          .required(t('manage.activityWizard.escapeRoomTimeLimitRequired'))
+          .integer(t('manage.activityWizard.mustBeInteger'))
+          .positive(t('manage.activityWizard.escapeRoomTimeLimitPositive')),
       otherwise: (schema) => schema.notRequired(),
     }),
     escapeRoomHintPenalty: yup.number().when('isEscapeRoom', {
       is: true,
       then: (schema) =>
         schema
-          .required('Hint penalty is required')
-          .integer('Must be an integer')
-          .min(0, 'Must be a non-negative number of seconds'),
+          .required(t('manage.activityWizard.escapeRoomHintPenaltyRequired'))
+          .integer(t('manage.activityWizard.mustBeInteger'))
+          .min(0, t('manage.activityWizard.escapeRoomHintPenaltyNonNegative')),
       otherwise: (schema) => schema.notRequired(),
     }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningWizard.tsx`
around lines 140 - 158, Replace the hardcoded escape-room validation messages in
the schema fields within MicroLearningWizard with the existing i18n t() helper,
using the appropriate translation keys; apply the same change to the
corresponding validation schema in PracticeQuizWizard.
apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx-247-249 (1)

247-249: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Default escapeRoomHintPenalty of '0' mismatches the backend default of 120 seconds.

The form defaults escapeRoomHintPenalty to '0' (no penalty), but the backend fallback in manipulateGroupActivity is 120 seconds. Since the frontend always sends a concrete value when isEscapeRoom is true, the backend's ?? 120 fallback is never reached. This means users get 0 seconds penalty by default instead of the intended 120. If 0 is intentional (letting users opt-in to a penalty), consider aligning the backend fallback. If 120 is the intended default, update the form default to '120'.

🔧 Proposed fix (if 120 seconds is the intended default)
     isEscapeRoom: false,
     escapeRoomTimeLimit: '60',
-    escapeRoomHintPenalty: '0',
+    escapeRoomHintPenalty: '120',
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx`
around lines 247 - 249, Align the escape-room hint penalty defaults: update the
initial form state in GroupActivityWizard, specifically escapeRoomHintPenalty,
from '0' to '120' so it matches the backend default used by
manipulateGroupActivity.
apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx-149-167 (1)

149-167: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use t() for validation error messages instead of hardcoded English strings.

The validation messages 'Time limit is required', 'Must be an integer', 'Must be a positive number of minutes', 'Hint penalty is required', and 'Must be a non-negative number of seconds' are hardcoded. The rest of this file uses t() for all validation messages. These should be localized for consistency.

🌐 Proposed fix for i18n validation messages
     escapeRoomTimeLimit: yup.number().when('isEscapeRoom', {
       is: true,
       then: (schema) =>
         schema
-          .required('Time limit is required')
-          .integer('Must be an integer')
-          .positive('Must be a positive number of minutes'),
+          .required(t('manage.activityWizard.escapeRoomTimeLimitRequired'))
+          .integer(t('manage.activityWizard.mustBeInteger'))
+          .positive(t('manage.activityWizard.escapeRoomTimeLimitPositive')),
       otherwise: (schema) => schema.notRequired(),
     }),
     escapeRoomHintPenalty: yup.number().when('isEscapeRoom', {
       is: true,
       then: (schema) =>
         schema
-          .required('Hint penalty is required')
-          .integer('Must be an integer')
-          .min(0, 'Must be a non-negative number of seconds'),
+          .required(t('manage.activityWizard.escapeRoomHintPenaltyRequired'))
+          .integer(t('manage.activityWizard.mustBeInteger'))
+          .min(0, t('manage.activityWizard.escapeRoomHintPenaltyNonNegative')),
       otherwise: (schema) => schema.notRequired(),
     }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx`
around lines 149 - 167, Replace the hardcoded validation messages in the
escapeRoomTimeLimit and escapeRoomHintPenalty Yup schemas within the
GroupActivityWizard validation schema with localized t() calls, matching the
existing translation key and interpolation conventions used elsewhere in the
file.
apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivitySettingsStep.tsx-153-190 (1)

153-190: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use t() for all user-facing labels instead of hardcoded English strings.

The labels "Escape Room Mode", "Time Limit (minutes)", and "Hint Penalty (seconds)" are hardcoded. The rest of this component uses t() from useTranslations() for all user-facing text. These should be localized to maintain consistency and support non-English locales.

🌐 Proposed fix for i18n labels
                     <Checkbox
-                      label="Escape Room Mode"
+                      label={t('manage.activityWizard.escapeRoomMode')}
                       checked={!!values.isEscapeRoom}
                       onCheck={() =>
                         setFieldValue('isEscapeRoom', !values.isEscapeRoom)
                       }
                       className={{
                         indicator: 'text-xs',
                         root: 'w-4.5 h-4.5',
                       }}
                       data={{ cy: 'toggle-escape-room' }}
                     />
                     {values.isEscapeRoom && (
                       <>
                         <FormikNumberField
                           name="escapeRoomTimeLimit"
-                          label="Time Limit (minutes)"
+                          label={t('manage.activityWizard.escapeRoomTimeLimit')}
                           required
                           className={{
                             root: 'w-full',
                             field: 'w-full',
                           }}
                           data={{ cy: 'escape-room-time-limit' }}
                         />
                         <FormikNumberField
                           name="escapeRoomHintPenalty"
-                          label="Hint Penalty (seconds)"
+                          label={t('manage.activityWizard.escapeRoomHintPenalty')}
                           required
                           className={{
                             root: 'w-full',
                             field: 'w-full',
                           }}
                           data={{ cy: 'escape-room-hint-penalty' }}
                         />
                       </>
                     )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivitySettingsStep.tsx`
around lines 153 - 190, Replace the hardcoded labels in the Escape Room section
with localized strings from the component’s existing useTranslations() hook:
update the Checkbox label and both FormikNumberField labels while preserving
their current meaning and translation namespace.
packages/graphql/src/services/groups.ts-1508-1531 (1)

1508-1531: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Enforce lockoutUntil before accepting another submission.

Line 1699 sets a lockout, but this path only checks IN_PROGRESS; an incorrect group can immediately submit again. Reject active attempts whose lockoutUntil is still in the future before persisting responses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/groups.ts` around lines 1508 - 1531, Before
accepting or persisting another escape-room submission in the active-attempt
validation, check `attempt.lockoutUntil` and reject when it is later than the
current time. Add this guard alongside the existing `IN_PROGRESS` and expiration
checks in the escape-room handling block, before response persistence, using the
same GraphQLError pattern.
packages/graphql/src/services/groups.ts-1617-1718 (1)

1617-1718: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Validate the complete expected answer set.

allCorrect starts as true and only iterates supplied responses. An empty or partial request can therefore complete the escape room without answering every required question. Derive required instance IDs from the activity stack, validate ownership, and require each required answer before completing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/groups.ts` around lines 1617 - 1718, Update the
escape-room validation around allCorrect and the response loop to derive every
required element instance ID from the activity stack, verify those instances
belong to the current group activity, and require a submitted response for each
required instance. Treat empty or partial responses as incorrect, while
preserving the existing per-type correctness evaluation before allowing
decisionsSubmittedAt or COMPLETED status.
apps/response-api/src/escapeRoom.ts-71-99 (1)

71-99: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not create attempts before verifying activity access and enrollment.

This response path creates an attempt from any valid participant token without the course-enrollment checks in startEscapeRoomAttempt. Require the pre-authorized attempt, or perform the same enrollment and activity checks before creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/response-api/src/escapeRoom.ts` around lines 71 - 99, The response path
creates a new escape-room attempt before verifying course enrollment and
activity access. In the attempt lookup/creation flow near getParticipantData,
require an existing pre-authorized attempt, or reuse the enrollment and activity
validation performed by startEscapeRoomAttempt before calling
escapeRoomAttempt.create.
apps/response-api/src/escapeRoom.ts-117-126 (1)

117-126: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply the configured latency grace consistently.

startEscapeRoomAttempt expires attempts only after totalLimit + 5, but this endpoint rejects responses immediately after totalLimit. A submission accepted by the lifecycle/countdown contract can therefore be rejected here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/response-api/src/escapeRoom.ts` around lines 117 - 126, Update the
expiration check in the relevant response-handling function to apply the same
5-second latency grace used by startEscapeRoomAttempt: compare elapsed time
against totalLimit + 5 before marking the attempt EXPIRED and returning
escape_room_expired. Keep the existing penalty calculation and expiration update
behavior unchanged.
apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx-119-157 (1)

119-157: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset local navigation state when restarting an attempt.

onReset leaves currentIx and progressState unchanged, while the synchronization effect only promotes entries to Correct. onStart also bypasses handleStartQuiz. Restarting from a later stack can therefore close the overlay onto a missing stack or preserve stale completion state. Clear local progress/index on reset and wire overlay start through handleStartQuiz.

Also applies to: 188-197

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx` around lines
119 - 157, Reset local navigation state in onReset by clearing progressState and
setting currentIx to the initial stack index, and ensure overlay onStart invokes
handleStartQuiz rather than bypassing it. Update the corresponding restart flow
near the additional referenced section so stale completion state and invalid
stack selection cannot persist.
packages/graphql/src/services/practiceQuizzes.ts-75-107 (1)

75-107: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not expose all escape-room stacks to non-participants.

Anonymous and temporary-participant requests bypass this branch and receive the unfiltered published quiz, including future stacks. Return no stacks unless the caller is an authorized preview user or a participant with a valid attempt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/practiceQuizzes.ts` around lines 75 - 107,
Update the escape-room filtering logic in the quiz stack resolver to deny
anonymous and temporary-participant callers by default. In the branch around
escapeRoomConfig and the attempt lookup, return no stacks unless the user is an
authorized preview user or has a valid participant attempt; retain
progress-based filtering for valid IN_PROGRESS attempts and prevent unfiltered
published stacks from being returned.
apps/response-api/src/escapeRoom.ts-128-187 (1)

128-187: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Reject or grade every supported escape-room element type.

Only SC, MC, KPRIM, numerical, and free-text responses are graded. Selection, case-study, QR-scan, content, and any other active-block types retain pointsPercentage = 0, making those stages impossible to clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/response-api/src/escapeRoom.ts` around lines 128 - 187, The grading
logic in the response handler only handles SC, MC, KPRIM, NUMERICAL, and
FREE_TEXT, leaving other supported active-block types at zero. Extend the type
dispatch after parsing solutions to explicitly handle selection, case-study,
QR-scan, content, and every other supported escape-room type using the
appropriate grading or validation behavior; unsupported or malformed types must
be rejected with the existing error response rather than silently graded as
zero.
packages/graphql/src/services/microLearning.ts-65-107 (1)

65-107: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Gate escape-room content for anonymous and temporary participants.

This branch filters stacks only for PARTICIPANT. Published escape rooms requested without a user or with a temporary participant bypass it and receive every stack, defeating sequential content gating.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/microLearning.ts` around lines 65 - 107, Update
the access condition in the microLearning resolver to apply escape-room stack
filtering for anonymous users and temporary participants, not only users with
role PARTICIPANT. Reuse the existing attempt lookup and status logic in the
escape-room branch, while preserving owner-specific behavior and ensuring users
without a valid attempt cannot receive stacks.
packages/graphql/src/services/practiceQuizzes.ts-897-1060 (1)

897-1060: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Require exactly one discriminated attempt target.

The API accepts multiple optional IDs. startEscapeRoomAttempt validates only the first matching branch but persists every supplied relation, while reset uses precedence-based ternaries. Reject zero/multiple targets and resolve one typed target before authorization, lookup, and mutation. Extracting that resolver also addresses the reported cognitive-complexity failures.

Also applies to: 1062-1161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/practiceQuizzes.ts` around lines 897 - 1060,
The startEscapeRoomAttempt API must require exactly one non-null target ID
instead of selecting the first matching branch and persisting all supplied
relations. Add a resolver near StartEscapeRoomAttemptArgs that validates
one-and-only-one of practiceQuizId, microLearningId, groupActivityId, or
elementBlockId and returns a typed discriminated target; use it for
authorization, activity lookup, attempt lookup, and creation. Apply the same
target resolution to the reset logic around resetEscapeRoomAttempt, replacing
precedence-based ternaries and reducing its cognitive complexity.

Source: Linters/SAST tools

apps/response-api/src/escapeRoom.ts-57-240 (1)

57-240: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split this validation pipeline into focused helpers.

Extract authentication/attempt resolution, expiry checks, grading, and result persistence. Sonar reports cognitive complexity 34 against the allowed 15, and the current monolith obscures the security-critical branch contracts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/response-api/src/escapeRoom.ts` around lines 57 - 240, Refactor
handleEscapeRoomValidation into focused helpers for participant authentication
and attempt creation/lookup, expiry and lockout validation, response grading,
and correct/incorrect result handling. Preserve each existing response status,
Redis key behavior, event payload, and Prisma update contract; keep
handleEscapeRoomValidation as the coordinator that invokes these helpers and
returns immediately when a helper has handled the response. Use the existing
symbols handleEscapeRoomValidation, getParticipantData, the grading functions,
and the Redis/Prisma operations to make the security-critical branches explicit
and reduce cognitive complexity below 15.

Source: Linters/SAST tools

packages/graphql/src/services/practiceQuizzes.ts-1163-1202 (1)

1163-1202: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reset the attempt and progress atomically.

The attempt is deleted before responses, outside a transaction, and deletion errors are suppressed. A partial failure can leave an old attempt with cleared answers—or a new attempt with stale correct answers that immediately unlock later stacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/practiceQuizzes.ts` around lines 1163 - 1202,
Make the reset operation atomic in the relevant practice-quiz reset function:
execute the EscapeRoomAttempt deletion and associated groupActivityInstance or
questionResponse cleanup inside a single Prisma transaction, preserving the
existing conditional filters. Remove the empty catch handlers so any deletion
failure aborts and rolls back the entire transaction, preventing partial reset
state.
packages/graphql/src/services/microLearning.ts-411-418 (1)

411-418: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not suppress escape-room configuration deletion failures.

Catching every error can report a successful edit while leaving escape-room mode enabled. Use an idempotent deletion or suppress only the expected “not found” case.

Proposed fix
       if (!isEscapeRoom && id) {
-        await prisma.escapeRoomConfig
-          .delete({
-            where: { microLearningId: id },
-          })
-          .catch(() => {})
+        await prisma.escapeRoomConfig.deleteMany({
+          where: { microLearningId: id },
+        })
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/microLearning.ts` around lines 411 - 418,
Update the escape-room configuration cleanup in the micro-learning edit flow to
avoid suppressing all deletion errors. Use an idempotent delete operation, or
catch only the expected record-not-found error while allowing other failures to
propagate; preserve the existing isEscapeRoom and id checks.
packages/graphql/src/services/practiceQuizzes.ts-371-378 (1)

371-378: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not ignore configuration deletion failures.

An unexpected deletion failure is swallowed, so the mutation can leave escape-room mode enabled despite saving it as disabled.

Proposed fix
       if (!isEscapeRoom && id) {
-        await prisma.escapeRoomConfig
-          .delete({
-            where: { practiceQuizId: id },
-          })
-          .catch(() => {})
+        await prisma.escapeRoomConfig.deleteMany({
+          where: { practiceQuizId: id },
+        })
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/practiceQuizzes.ts` around lines 371 - 378, In
the isEscapeRoom configuration cleanup within the practice quiz mutation, do not
swallow errors from prisma.escapeRoomConfig.delete. Remove the empty catch and
allow the deletion failure to propagate so the mutation cannot report a disabled
escape room while its configuration remains enabled.
apps/response-api/src/index.ts-134-149 (1)

134-149: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Misleading comment and unnecessary Redis overhead on every response.

Two concerns:

  1. The comment "Synchronous Escape Room Lockout & Correctness Check" is misleading — handleEscapeRoomValidation is async and the call uses await.

  2. redis.hgetall(${instanceKey}:info) runs on every response submission. When the key doesn't exist, Redis returns an empty object {} (truthy in JS), so if (instanceInfo) passes and handleEscapeRoomValidation is called, only to immediately return false when instanceInfo.isEscapeRoom !== 'true'. This adds a Redis round-trip to the hot path for all non-escape-room responses. Consider checking a lighter-weight signal first (e.g., redis.hget(key, 'isEscapeRoom')) or gating the entire block behind a cheaper condition.

Additionally, per the implementation review at project/2026-07-10-pr-5143-escape-room-implementation-review.md (M8), this live-quiz escape path is unreachable dead code — no wizard configures live-quiz escape room mode, so instanceInfo.isEscapeRoom will never be 'true' in production. Either finish the live-quiz authoring surface or remove this path for v1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/response-api/src/index.ts` around lines 134 - 149, The escape-room
validation block has a misleading synchronous comment, performs an unnecessary
Redis hgetall on every response, and is unreachable without live-quiz authoring
support. Replace the comment with accurate async wording, then either remove the
live-quiz path for v1 or implement the required authoring/configuration support;
if retained, gate handleEscapeRoomValidation behind a lightweight isEscapeRoom
check using the instance info key and preserve the await behavior.
playwright/tests/Z-escape-room.spec.ts-65-102 (1)

65-102: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fragile timer selector and insufficient test coverage for core escape-room mechanics.

The loginStudent call at lines 67–70 passes a superfluous second argument, previously flagged by github-code-quality[bot].

The timer assertion at lines 88–92 uses page.locator('span:has-text(":")').first(), which is unscoped — any span containing a colon on the page matches. Scope it to the timer banner, e.g., page.locator('[data-testid="escape-room-timer"] span') or a more specific selector.

The test only covers a single-stack quiz. The core escape-room mechanic — sequential gating (stack N+1 locked until stack N correct) — is untested. Expand to ≥3 stacks and add cases for: wrong-answer lockout, preceding-stack rejection, and expiry overlay. This is also recommended in the implementation review §6 #9.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playwright/tests/Z-escape-room.spec.ts` around lines 65 - 102, Remove the
unnecessary second argument from loginStudent in the Student Solves Escape Room
Practice Quiz test. Scope the timer assertion to the escape-room timer element,
such as escape-room-timer, instead of an arbitrary colon-containing span. Expand
coverage to at least three stacks, verifying wrong-answer lockout, rejection of
attempts on later stacks before the preceding stack is solved, and the expiry
overlay, while retaining the successful sequential completion flow.
apps/frontend-pwa/src/components/practiceQuiz/EscapeRoomOverlay.tsx-42-102 (1)

42-102: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Blocking overlays lack dialog semantics and focus management.

The start, expired, and completed overlays (lines 43–179) use fixed inset-0 z-[100] to cover the full page but have no role="dialog", aria-modal="true", or focus-trap. Background content remains tab-reachable, making these modals inaccessible to keyboard and screen-reader users.

Add role="dialog" and aria-modal="true" to the outer overlay divs, trap focus within the modal, and associate the <H3> title via aria-labelledby.

Additionally, bg-slate-750 (line 61) is not a standard Tailwind class — Tailwind's slate palette jumps from 700 to 800. Unless the project's Tailwind config extends this, the class has no effect. Verify or use bg-slate-700.

This is also documented in the implementation review at project/2026-07-10-pr-5143-escape-room-implementation-review.md §4 (A11y).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-pwa/src/components/practiceQuiz/EscapeRoomOverlay.tsx` around
lines 42 - 102, Add dialog semantics and keyboard focus trapping to the start,
expired, and completed overlay containers in EscapeRoomOverlay: add
role="dialog", aria-modal="true", and unique aria-labelledby references
connected to each H3 title, using the project’s focus-trap pattern or an
equivalent implementation so background content is not tab-reachable. Also
replace bg-slate-750 with bg-slate-700 unless the Tailwind configuration
explicitly defines the custom class.
packages/graphql/src/services/pruneEscapeRooms.ts-85-122 (1)

85-122: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Failed stats updates silently lose data — attempts are deleted regardless.

The .catch() blocks at lines 99 and 117 log errors but don't prevent the deleteMany at line 128 from deleting all attempts. If a stats update or create fails for any instance, the attempt is still pruned, permanently losing the statistics for that instance. Additionally, the entire operation is non-transactional: if the process crashes after some stats are updated but before deleteMany, the next run will re-process those attempts and double-count their statistics.

Consider tracking which attempts had all stats successfully updated and only deleting those, or wrapping each attempt's stats-update + deletion in a Prisma transaction.

🔒 Proposed fix: only delete fully-processed attempts
     for (const attempt of attempts) {
+      let allStatsSucceeded = true
       // Find element instances related to this attempt
       let instances: { id: number }[] = []
       // ... existing instance-finding logic ...

       for (const instance of instances) {
         const stats = await globalContext.prisma.instanceStatistics.findUnique({
           where: { elementInstanceId: instance.id },
         })

         if (stats) {
           // ... existing update logic ...
-          .catch((err) => {
-            executionContext.logger.error(
-              `Failed to update stats for instance ${instance.id}: ${err}`
-            )
-          })
+          .catch((err) => {
+            allStatsSucceeded = false
+            executionContext.logger.error(
+              `Failed to update stats for instance ${instance.id}: ${err}`
+            )
+          })
         } else {
           // ... existing create logic ...
-          .catch((err) => {
-            executionContext.logger.error(
-              `Failed to create stats for instance ${instance.id}: ${err}`
-            )
-          })
+          .catch((err) => {
+            allStatsSucceeded = false
+            executionContext.logger.error(
+              `Failed to create stats for instance ${instance.id}: ${err}`
+            )
+          })
         }
       }
+      attempt._statsSucceeded = allStatsSucceeded
+    }
+
+    // Delete only fully-processed attempts
+    const successfulAttemptIds = attempts
+      .filter((a) => a._statsSucceeded)
+      .map((a) => a.id)
+
+    if (successfulAttemptIds.length > 0) {
+      await globalContext.prisma.escapeRoomAttempt.deleteMany({
+        where: { id: { in: successfulAttemptIds } },
+      })
+      executionContext.logger.info(
+        `Successfully pruned ${successfulAttemptIds.length} of ${attempts.length} attempts`
+      )
+      if (successfulAttemptIds.length < attempts.length) {
+        executionContext.logger.warn(
+          `${attempts.length - successfulAttemptIds.length} attempts retained due to stats update failures`
+        )
+      }
     }

-    // Delete pruned attempts
-    if (attempts.length > 0) {
-      await globalContext.prisma.escapeRoomAttempt.deleteMany({
-        where: {
-          id: { in: attempts.map((a) => a.id) },
-        },
-      })
-      executionContext.logger.info(
-        `Successfully pruned ${attempts.length} attempts`
-      )
-    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/pruneEscapeRooms.ts` around lines 85 - 122,
Stats persistence failures are swallowed while deleteMany still removes the
related attempts, risking data loss and duplicate processing. Update the pruning
logic in the surrounding stats-processing function to track attempts whose
update/create operations completed successfully, propagate or record failures,
and restrict deleteMany to only those fully processed attempts. Prefer wrapping
each attempt’s stats write and deletion in a Prisma transaction to make
processing atomic and prevent reprocessing after partial failures.
🟡 Minor comments (6)
packages/graphql/src/schema/practiceQuiz.ts-289-294 (1)

289-294: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate hintsUsed before returning it
packages/graphql/src/schema/practiceQuiz.ts:289-294
hintsUsed is stored as JSON, so a direct cast can pass through non-arrays or mixed values and break the [String!]! contract. Guard with Array.isArray(...) and filter to strings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/practiceQuiz.ts` around lines 289 - 294, Update
the hintsUsed field resolver to validate the JSON value with Array.isArray
before returning it, and filter the array to retain only string elements; return
an empty array for non-array values to preserve the [String!]! contract.
packages/graphql/src/schema/groupActivity.ts-77-88 (1)

77-88: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a deterministic group lookup here

ParticipantGroup is only unique by courseId + code, and nothing in the membership flow prevents a participant from being in multiple groups in the same course. findFirst can therefore pick an arbitrary group and return the wrong attempts. Resolve the group through the activity’s group instance or enforce single-group membership per course.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/groupActivity.ts` around lines 77 - 88, Replace
the findFirst lookup in the participant-group attempt resolver with a
deterministic lookup tied to the activity’s group instance, using the
activity/group relationship and the current participant membership;
alternatively enforce single-group membership per course and use that invariant
consistently. Update the escapeRoomAttempt query to use the resolved group ID
and preserve the empty result when no valid group exists.
apps/frontend-pwa/src/pages/group/[groupId]/activity/[activityId].tsx-26-27 (1)

26-27: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use configured aliases for the new local imports.

Replace the relative traversals with the corresponding @/~ aliases. As per coding guidelines, “Use @ and ~ path aliases for imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-pwa/src/pages/group/`[groupId]/activity/[activityId].tsx around
lines 26 - 27, Replace the relative imports for useEscapeRoom and
EscapeRoomOverlay with the repository’s configured @ or ~ path aliases,
preserving the existing imported symbols.

Source: Coding guidelines

apps/frontend-pwa/src/components/liveQuiz/QuestionArea.tsx-240-259 (1)

240-259: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add translations for the new escape-room toasts.

pwa.practiceQuiz.escapeRoomIncorrectToast and escapeRoomLockoutToast are not present in packages/i18n/messages/de.ts lines 801-868, so German users receive the English defaults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-pwa/src/components/liveQuiz/QuestionArea.tsx` around lines 240
- 259, Add German translations for the keys
pwa.practiceQuiz.escapeRoomIncorrectToast and
pwa.practiceQuiz.escapeRoomLockoutToast in the practiceQuiz translations within
de.ts, preserving the existing message structure so showStatusCodeToast displays
localized text instead of falling back to English.
apps/frontend-pwa/src/components/hooks/useEscapeRoom.ts-77-84 (1)

77-84: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Stop polling after local expiry.

If the server still returns IN_PROGRESS, this calls refetch() every second after the timer reaches zero. Refetch once on the transition to zero, then clear the interval or guard with a ref.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-pwa/src/components/hooks/useEscapeRoom.ts` around lines 77 -
84, In the useEscapeRoom timer logic, prevent repeated refetching after local
expiry by tracking whether the countdown has already reached zero. In
calculateRemaining, trigger refetch only on the transition to zero, then clear
the interval or use a ref guard so subsequent ticks do nothing; ensure the
interval cleanup remains intact.
apps/frontend-pwa/src/components/practiceQuiz/EscapeRoomOverlay.tsx-181-199 (1)

181-199: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add aria-live to the countdown timer for screen reader accessibility.

The countdown <span> (line 193) has no aria-live attribute. Screen readers won't announce time updates, which is critical for a time-limited escape room. Add aria-live="polite" to the timer container so screen readers announce remaining time at appropriate intervals.

Also, remainingSeconds === null renders '00:00', which is ambiguous — it could mean "expired" or "not yet initialized." Consider rendering the initial timeLimit formatted value instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/frontend-pwa/src/components/practiceQuiz/EscapeRoomOverlay.tsx` around
lines 181 - 199, Add aria-live="polite" to the countdown timer container in
EscapeRoomOverlay’s active in-progress render so screen readers announce
updates. Replace the null fallback in the remainingSeconds display with the
formatted initial timeLimit value, avoiding an ambiguous 00:00 before
initialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8247ced6-d4c1-464d-b393-6964d063d7be

📥 Commits

Reviewing files that changed from the base of the PR and between bd6df48 and 5af470d.

⛔ Files ignored due to path filters (22)
  • packages/graphql/src/graphql/ops/FMicroLearningData.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/FMicroLearningDataWithoutSolutions.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/FPracticeQuizData.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/FPracticeQuizDataWithoutSolutions.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MCreateGroupActivity.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MCreateMicroLearning.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MCreatePracticeQuiz.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MEditGroupActivity.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MEditMicroLearning.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MEditPracticeQuiz.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MResetEscapeRoomAttempt.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/MStartEscapeRoomAttempt.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/QGetActivityTemplate.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/QGetCourseGroupActivities.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/QGetGroupActivity.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/graphql/ops/QGroupActivityDetails.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/ops.schema.json is excluded by !**/**/ops.schema.json
  • packages/graphql/src/ops.ts is excluded by !**/**/ops.ts
  • packages/graphql/src/public/client.json is excluded by !**/**/public/**
  • packages/graphql/src/public/schema.graphql is excluded by !**/**/public/**
  • packages/graphql/src/public/server.json is excluded by !**/**/public/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (56)
  • apps/analytics/prisma/schema/element.prisma
  • apps/analytics/prisma/schema/participant.prisma
  • apps/analytics/prisma/schema/quiz.prisma
  • apps/frontend-manage/src/components/activities/ActivityCreation.tsx
  • apps/frontend-manage/src/components/activities/creation/WizardLayout.tsx
  • apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivitySettingsStep.tsx
  • apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx
  • apps/frontend-manage/src/components/activities/creation/groupActivity/submitGroupActivityForm.ts
  • apps/frontend-manage/src/components/activities/creation/liveQuiz/LiveQuizWizard.tsx
  • apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningSettingsStep.tsx
  • apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningWizard.tsx
  • apps/frontend-manage/src/components/activities/creation/microLearning/submitMicrolearningForm.ts
  • apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizSettingsStep.tsx
  • apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx
  • apps/frontend-manage/src/components/activities/creation/practiceQuiz/submitPracticeQuizForm.ts
  • apps/frontend-manage/src/pages/index.tsx
  • apps/frontend-pwa/src/components/hooks/useEscapeRoom.ts
  • apps/frontend-pwa/src/components/liveQuiz/QuestionArea.tsx
  • apps/frontend-pwa/src/components/practiceQuiz/EscapeRoomOverlay.tsx
  • apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx
  • apps/frontend-pwa/src/pages/course/[courseId]/microLearnings/[id]/[ix].tsx
  • apps/frontend-pwa/src/pages/course/[courseId]/practiceQuizzes/[id].tsx
  • apps/frontend-pwa/src/pages/group/[groupId]/activity/[activityId].tsx
  • apps/response-api/package.json
  • apps/response-api/src/escapeRoom.ts
  • apps/response-api/src/index.ts
  • docker-compose.yml
  • docs/domain-model.md
  • docs/log.md
  • docs/testing.md
  • packages/graphql/src/index.ts
  • packages/graphql/src/schema/escapeRoomConfig.ts
  • packages/graphql/src/schema/groupActivity.ts
  • packages/graphql/src/schema/liveQuiz.ts
  • packages/graphql/src/schema/microLearning.ts
  • packages/graphql/src/schema/mutation.ts
  • packages/graphql/src/schema/practiceQuiz.ts
  • packages/graphql/src/services/courses.ts
  • packages/graphql/src/services/groups.ts
  • packages/graphql/src/services/liveQuizzes.ts
  • packages/graphql/src/services/microLearning.ts
  • packages/graphql/src/services/practiceQuizzes.ts
  • packages/graphql/src/services/pruneEscapeRooms.ts
  • packages/graphql/src/services/stacks.ts
  • packages/graphql/test/helpers.ts
  • packages/hatchet/src/index.ts
  • packages/i18n/messages/de.ts
  • packages/i18n/messages/en.ts
  • packages/prisma/src/prisma/schema/element.prisma
  • packages/prisma/src/prisma/schema/migrations/20260707103049_add_escape_room_mode/migration.sql
  • packages/prisma/src/prisma/schema/participant.prisma
  • packages/prisma/src/prisma/schema/quiz.prisma
  • packages/types/src/hatchet.ts
  • playwright/tests/Z-escape-room.spec.ts
  • project/2026-07-07-pr-5143-escape-room-quiz-mode-plan.md
  • project/2026-07-10-pr-5143-escape-room-implementation-review.md

Comment thread apps/frontend-pwa/src/pages/course/[courseId]/microLearnings/[id]/[ix].tsx Outdated
Comment thread docker-compose.yml Outdated
Comment thread packages/graphql/src/services/stacks.ts
Comment thread playwright/tests/Z-escape-room.spec.ts Outdated
Comment thread apps/response-api/src/escapeRoom.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/graphql/src/services/pruneEscapeRooms.ts (1)

10-173: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split this workflow to satisfy the complexity gate.

SonarCloud reports cognitive complexity 43 versus the allowed 15. Extract instance resolution, metric calculation, per-attempt aggregation, and housekeeping into focused functions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/pruneEscapeRooms.ts` around lines 10 - 173,
Reduce the cognitive complexity of handlePruneEscapeRooms by extracting focused
helpers for resolving instances, calculating attempt metrics, aggregating
statistics for one attempt, and pruning stale attempts. Keep
handlePruneEscapeRooms as orchestration: fetch pending attempts, invoke the
per-attempt aggregation helper, perform housekeeping, log results, and handle
top-level errors; preserve existing logging, error handling, and database
behavior.

Source: Linters/SAST tools

packages/graphql/src/services/practiceQuizzes.ts (1)

1138-1167: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass the reset target into the lecturer reset path. packages/graphql/src/services/practiceQuizzes.ts:1145-1167 The caller only sends the activity id, so participantId/groupId are still undefined here. That makes the selector fall through to a branch with participantId!/elementBlockId! undefined, the delete gets swallowed, and the mutation returns true without clearing anything. Require the target attempt identifier before building attemptWhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/practiceQuizzes.ts` around lines 1138 - 1167,
The lecturer reset path builds an invalid selector because reset target
identifiers are not passed through from the caller. Update the relevant reset
mutation/callers and the lecturer reset logic around attemptWhere to require and
forward the target participantId/groupId (and associated activity identifier),
then validate the target before constructing attemptWhere so it cannot fall
through with undefined values; preserve the existing GraphQLError behavior for
missing or invalid targets.
🧹 Nitpick comments (1)
packages/graphql/src/services/practiceQuizzes.ts (1)

20-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a configured path alias for checkAccess.

Replace ./sharing.js with the repository’s corresponding @ or ~ alias.

As per coding guidelines, **/*.{ts,tsx,js,jsx,mts,mjs,cjs} imports must use @ and ~ path aliases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/practiceQuizzes.ts` at line 20, Update the
checkAccess import in practiceQuizzes.ts to use the repository’s configured @ or
~ path alias instead of the relative './sharing.js' path, preserving the
existing symbol and extension conventions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/graphql/src/services/pruneEscapeRooms.ts`:
- Around line 151-162: Update the cleanup query in the prune logic to anchor
retention to when the attempt finished or was aggregated, not started. In the
deleteMany filter, replace the startedAt cutoff condition with a cutoff on
statsAggregatedAt (or the explicit completion/expiry timestamp if available),
while retaining the completed/expired statuses and non-null aggregation
requirement.
- Around line 67-125: The statistics update in the escape-room pruning logic
incorrectly applies activity-wide timeSpent, isSuccess, and triesCount to every
instance. Refactor the logic around these calculations and the
instanceStatistics update/create operations to use metrics specific to each
instance, deriving submission or retry counts from per-instance attempt data
rather than hintsUsed and penaltySeconds; do not persist the quiz’s total
duration or final status for every element.
- Around line 22-29: Update the processing logic in the escape-room pruning
service to handle each attempt within a Prisma transaction: perform all
instanceStatistics aggregation writes and set statsAggregatedAt only after every
write succeeds, allowing failures to roll back without marking the attempt
complete. Ensure the find-and-claim or processing flow prevents concurrent jobs
from handling the same unclaimed attempt, using the relevant escapeRoomAttempt
and transaction methods.

---

Outside diff comments:
In `@packages/graphql/src/services/practiceQuizzes.ts`:
- Around line 1138-1167: The lecturer reset path builds an invalid selector
because reset target identifiers are not passed through from the caller. Update
the relevant reset mutation/callers and the lecturer reset logic around
attemptWhere to require and forward the target participantId/groupId (and
associated activity identifier), then validate the target before constructing
attemptWhere so it cannot fall through with undefined values; preserve the
existing GraphQLError behavior for missing or invalid targets.

In `@packages/graphql/src/services/pruneEscapeRooms.ts`:
- Around line 10-173: Reduce the cognitive complexity of handlePruneEscapeRooms
by extracting focused helpers for resolving instances, calculating attempt
metrics, aggregating statistics for one attempt, and pruning stale attempts.
Keep handlePruneEscapeRooms as orchestration: fetch pending attempts, invoke the
per-attempt aggregation helper, perform housekeeping, log results, and handle
top-level errors; preserve existing logging, error handling, and database
behavior.

---

Nitpick comments:
In `@packages/graphql/src/services/practiceQuizzes.ts`:
- Line 20: Update the checkAccess import in practiceQuizzes.ts to use the
repository’s configured @ or ~ path alias instead of the relative './sharing.js'
path, preserving the existing symbol and extension conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e7f781b-7600-4d6a-8e65-f2c60ddc695a

📥 Commits

Reviewing files that changed from the base of the PR and between 5af470d and 8fd46c8.

📒 Files selected for processing (9)
  • apps/frontend-manage/src/components/activities/ActivityCreation.tsx
  • packages/graphql/src/services/groups.ts
  • packages/graphql/src/services/microLearning.ts
  • packages/graphql/src/services/practiceQuizzes.ts
  • packages/graphql/src/services/pruneEscapeRooms.ts
  • packages/graphql/src/services/stacks.ts
  • packages/prisma/src/prisma/schema/migrations/20260707103049_add_escape_room_mode/migration.sql
  • packages/prisma/src/prisma/schema/quiz.prisma
  • playwright/tests/Z-escape-room.spec.ts
💤 Files with no reviewable changes (1)
  • apps/frontend-manage/src/components/activities/ActivityCreation.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • playwright/tests/Z-escape-room.spec.ts
  • packages/prisma/src/prisma/schema/migrations/20260707103049_add_escape_room_mode/migration.sql
  • packages/graphql/src/services/stacks.ts
  • packages/prisma/src/prisma/schema/quiz.prisma
  • packages/graphql/src/services/microLearning.ts
  • packages/graphql/src/services/groups.ts

Comment thread packages/graphql/src/services/pruneEscapeRooms.ts Outdated
Comment thread packages/graphql/src/services/pruneEscapeRooms.ts Outdated
Comment thread packages/graphql/src/services/pruneEscapeRooms.ts Outdated
Comment thread apps/response-api/src/escapeRoom.ts Outdated
Comment thread apps/response-api/src/escapeRoom.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
apps/frontend-manage/src/components/activities/creation/EscapeRoomSettingsFields.tsx (1)

4-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mark props as read-only (SonarCloud).

Interface fields aren't mutated but aren't marked readonly either.

♻️ Proposed fix
-interface EscapeRoomSettingsFieldsProps {
-  isEscapeRoom: boolean
-  onToggle: (nextEnabled: boolean) => void
-}
+interface EscapeRoomSettingsFieldsProps {
+  readonly isEscapeRoom: boolean
+  readonly onToggle: (nextEnabled: boolean) => void
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/frontend-manage/src/components/activities/creation/EscapeRoomSettingsFields.tsx`
around lines 4 - 12, Mark the EscapeRoomSettingsFieldsProps interface properties
isEscapeRoom and onToggle as readonly, without changing the component’s behavior
or callback signature.

Source: Linters/SAST tools

packages/graphql/src/schema/practiceQuiz.ts (2)

195-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid any casts; the Prisma include already gives a typed shape.

♻️ Suggested fix
-        if (elements.length === 0) return false
-        return (elements as any[]).every((elem) =>
-          elem.responses?.some(
-            (resp: any) =>
-              resp.lastResponseCorrectness === DB.ResponseCorrectness.CORRECT
-          )
-        )
+        if (elements.length === 0) return false
+        return elements.every((elem) =>
+          elem.responses.some(
+            (resp) => resp.lastResponseCorrectness === DB.ResponseCorrectness.CORRECT
+          )
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/practiceQuiz.ts` around lines 195 - 199, In the
response-checking logic, remove the `any` casts from `elements` and `resp`; use
the Prisma-inferred type provided by the existing include and access
`elem.responses` and `resp.lastResponseCorrectness` directly, preserving the
current `every`/`some` behavior.

178-201: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Potential N+1 if isCorrect is queried across a list of stacks.

Each invocation issues its own elementInstance.findMany round-trip. If the escape-room UI requests isCorrect for every ElementStack in a list (e.g. an overview/map view), this becomes an N+1 query pattern, especially under periodic polling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/practiceQuiz.ts` around lines 178 - 201, The
isCorrect resolver performs a separate database query for every ElementStack,
creating an N+1 pattern in list queries. Refactor the practice quiz resolver
around isCorrect to batch element-instance lookups per request, using a
DataLoader or equivalent ctx-scoped cache keyed by elementStackId and
participantId, while preserving the existing empty-stack and correctness
behavior.
packages/graphql/src/schema/query.ts (1)

628-634: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract nested ternary in permission selector.

SonarCloud flags this nested ternary for readability. Extract into a small named function or if/else chain.

♻️ Suggested refactor
-        resolve: withPermission(
-          (args) =>
-            args.practiceQuizId
-              ? { practiceQuizId: args.practiceQuizId }
-              : args.microLearningId
-                ? { microLearningId: args.microLearningId }
-                : { groupActivityId: args.groupActivityId },
-          DB.PermissionLevel.READ,
+        resolve: withPermission(
+          (args) => {
+            if (args.practiceQuizId) return { practiceQuizId: args.practiceQuizId }
+            if (args.microLearningId) return { microLearningId: args.microLearningId }
+            return { groupActivityId: args.groupActivityId }
+          },
+          DB.PermissionLevel.READ,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/schema/query.ts` around lines 628 - 634, Replace the
nested ternary in the permission selector callback with a small named helper
function or clear if/else chain that selects practiceQuizId, microLearningId, or
groupActivityId, then pass the helper’s result to the existing permission
lookup.

Source: Linters/SAST tools

packages/graphql/src/services/escapeRooms.ts (1)

99-111: 🚀 Performance & Scalability | 🔵 Trivial

No pagination on the lecturer progress query.

escapeRoomAttempt.findMany returns every attempt for the activity in a single response with no take/cursor. Fine for typical class sizes, but worth keeping in mind if this dashboard is ever used for very large cohorts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/graphql/src/services/escapeRooms.ts` around lines 99 - 111, Add
pagination to the lecturer progress query in the escapeRoomAttempt.findMany
call, using an appropriate take/skip or cursor-based scheme and exposing the
necessary pagination inputs and metadata through the surrounding
resolver/service. Preserve the existing filters, includes, and ordering while
ensuring large cohorts are not returned in a single response.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivitySettingsStep.tsx`:
- Line 15: Replace the relative EscapeRoomSettingsFields import in
GroupActivitySettingsStep with the configured @ or ~ path alias, matching the
project’s existing alias conventions.

In
`@apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningSettingsStep.tsx`:
- Line 17: Replace the relative import of EscapeRoomSettingsFields in
MicroLearningSettingsStep.tsx with the configured @ or ~ path alias, matching
the project’s existing alias conventions.

In
`@apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizSettingsStep.tsx`:
- Line 16: Replace the relative EscapeRoomSettingsFields import in
PracticeQuizSettingsStep with the configured @ or ~ path alias, preserving the
existing module reference and behavior.

In `@packages/graphql/src/services/escapeRooms.ts`:
- Around line 56-205: Reduce the cognitive complexity of getEscapeRoomProgress
by extracting the cleared-stack calculation (including response loading and
participant/group fallback logic) and the per-attempt progress mapping into
standalone helpers. Move the nested ternary for clearedStacks into explicit
conditional logic within the mapping helper, then have getEscapeRoomProgress
orchestrate these helpers while preserving the existing behavior and returned
fields.

---

Nitpick comments:
In
`@apps/frontend-manage/src/components/activities/creation/EscapeRoomSettingsFields.tsx`:
- Around line 4-12: Mark the EscapeRoomSettingsFieldsProps interface properties
isEscapeRoom and onToggle as readonly, without changing the component’s behavior
or callback signature.

In `@packages/graphql/src/schema/practiceQuiz.ts`:
- Around line 195-199: In the response-checking logic, remove the `any` casts
from `elements` and `resp`; use the Prisma-inferred type provided by the
existing include and access `elem.responses` and `resp.lastResponseCorrectness`
directly, preserving the current `every`/`some` behavior.
- Around line 178-201: The isCorrect resolver performs a separate database query
for every ElementStack, creating an N+1 pattern in list queries. Refactor the
practice quiz resolver around isCorrect to batch element-instance lookups per
request, using a DataLoader or equivalent ctx-scoped cache keyed by
elementStackId and participantId, while preserving the existing empty-stack and
correctness behavior.

In `@packages/graphql/src/schema/query.ts`:
- Around line 628-634: Replace the nested ternary in the permission selector
callback with a small named helper function or clear if/else chain that selects
practiceQuizId, microLearningId, or groupActivityId, then pass the helper’s
result to the existing permission lookup.

In `@packages/graphql/src/services/escapeRooms.ts`:
- Around line 99-111: Add pagination to the lecturer progress query in the
escapeRoomAttempt.findMany call, using an appropriate take/skip or cursor-based
scheme and exposing the necessary pagination inputs and metadata through the
surrounding resolver/service. Preserve the existing filters, includes, and
ordering while ensuring large cohorts are not returned in a single response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fb39bda4-f85f-42e3-92d1-824e0a97446e

📥 Commits

Reviewing files that changed from the base of the PR and between 8fd46c8 and 6c1b0e7.

⛔ Files ignored due to path filters (6)
  • packages/graphql/src/graphql/ops/QGetEscapeRoomProgress.graphql is excluded by !**/**/graphql/ops/**
  • packages/graphql/src/ops.schema.json is excluded by !**/**/ops.schema.json
  • packages/graphql/src/ops.ts is excluded by !**/**/ops.ts
  • packages/graphql/src/public/client.json is excluded by !**/**/public/**
  • packages/graphql/src/public/schema.graphql is excluded by !**/**/public/**
  • packages/graphql/src/public/server.json is excluded by !**/**/public/**
📒 Files selected for processing (17)
  • apps/analytics/prisma/schema/quiz.prisma
  • apps/frontend-manage/src/components/activities/creation/EscapeRoomSettingsFields.tsx
  • apps/frontend-manage/src/components/activities/creation/escapeRoomValidation.ts
  • apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivitySettingsStep.tsx
  • apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx
  • apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningSettingsStep.tsx
  • apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningWizard.tsx
  • apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizSettingsStep.tsx
  • apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx
  • packages/graphql/src/schema/practiceQuiz.ts
  • packages/graphql/src/schema/query.ts
  • packages/graphql/src/services/escapeRooms.ts
  • packages/graphql/test/escapeRoom.test.ts
  • packages/graphql/test/helpers.ts
  • packages/i18n/messages/de.ts
  • packages/i18n/messages/en.ts
  • project/2026-07-10-pr-5143-escape-room-implementation-review.md
✅ Files skipped from review due to trivial changes (1)
  • packages/i18n/messages/en.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/frontend-manage/src/components/activities/creation/groupActivity/GroupActivityWizard.tsx
  • apps/frontend-manage/src/components/activities/creation/practiceQuiz/PracticeQuizWizard.tsx
  • apps/analytics/prisma/schema/quiz.prisma
  • apps/frontend-manage/src/components/activities/creation/microLearning/MicroLearningWizard.tsx

Comment thread packages/graphql/src/services/escapeRooms.ts Outdated
…nd validation bounds

- Extract EscapeRoomSettingsFields shared component (Checkbox + time limit + hint penalty) used by practice quiz, microlearning, group activity settings steps (M6)
- Extract useEscapeRoomYupFields hook with i18n-backed validation bounds: time limit integer/positive/max 1440min, hint penalty integer/min 0/max 3600s (M4)
- Force sequential order and disable the order selector when escape mode is enabled in practice quiz (M5)
- Add 10 escape room i18n keys to en + de (labels, tooltips, validation messages)
- Sync statsAggregatedAt to apps/analytics prisma schema (prisma:sync drift from B4)
…urer dashboard

- New escapeRooms.ts service: getEscapeRoomProgress aggregates all attempts on
  an escape-room activity (practice quiz / microlearning / group activity) into
  per-participant progress (cleared/total stacks, status, time spent, penalty,
  hints used, lockout) for the lecturer view.
- Per-stack cleared count reuses the getPracticeQuizData masking logic (leading
  fully-correct stacks); group attempts fall back to a status-derived estimate.
- EscapeRoomProgress / EscapeRoomAttemptProgress Pothos types + query registered
  with withPermission READ owner authorization; QGetEscapeRoomProgress op + codegen.
- elementBlock (live-quiz) path kept in the service for later; not exposed in the
  query until the live-quiz escape authoring surface (M8) lands.
…ng/lockout/expiry/completion)

- New packages/graphql/test/escapeRoom.test.ts (11 tests): B1 anon/non-participant
  grading guard + owner preview bypass, sequential stack gating, lockout window
  enforcement, attempt expiry, B2 lecturer-only reset (lecturer/participant/no-write
  access), completion fires exactly once, B4 prune retention window.
- New seedEscapeRoomPracticeQuiz helper (real elementData via processElementData so
  the SC grading path resolves CORRECT/INCORRECT instead of always-incorrect).
- Live-DB integration tests (CI-verified); throw messages traced verbatim from
  source, Participation + startEscapeRoomAttempt preconditions checked against source.
- Update review doc progress log.
…ction

# Conflicts:
#	docs/log.md
#	packages/graphql/src/ops.ts
#	packages/graphql/src/public/client.json
#	packages/graphql/src/public/server.json
#	packages/graphql/src/services/liveQuizzes.ts
# Conflicts:
#	apps/frontend-pwa/package.json
#	apps/hatchet-worker-general/package.json
#	apps/hatchet-worker-response-processor/package.json
#	apps/response-api/package.json
#	docs/log.md
#	packages/graphql/src/services/groups.ts
#	packages/graphql/test/helpers.ts
#	pnpm-lock.yaml
@rschlaefli rschlaefli changed the title feat(quiz): generalized escape room mode and response validation feat(escape-room): ship generalized quiz mode Jul 19, 2026
Comment thread apps/response-api/src/escapeRoom.ts Fixed
Comment thread packages/graphql/test/escapeRoomTestHarness.ts Fixed
Copilot AI review requested due to automatic review settings July 26, 2026 09:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings July 26, 2026 09:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature size:XXL This PR changes 1000+ lines, ignoring generated files.

Development

Successfully merging this pull request may close these issues.

3 participants