Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
85f7a45
feat(escape-room): establish generalized quiz mode baseline
rschlaefli Jul 11, 2026
d131d87
docs(wiki): update changelog log.md for escape room correctness and s…
rschlaefli Jul 8, 2026
79a50b6
docs(project): escape room implementation review and production roadmap
rschlaefli Jul 10, 2026
61bf7d8
fix(escape-room): green-baseline fixes (docker port, migration index,…
rschlaefli Jul 10, 2026
12018d2
fix(escape-room): close integrity holes (anon bypass, participant sel…
rschlaefli Jul 10, 2026
594158e
refactor(manage): share escape room config fields, i18n, order lock a…
rschlaefli Jul 10, 2026
50c38c8
feat(graphql): add owner-authorized escapeRoomProgress query for lect…
rschlaefli Jul 10, 2026
9e4fa9e
test(graphql): escape-room security regression tests (B1/B2/B4 + gati…
rschlaefli Jul 10, 2026
4082699
test(graphql): cover escapeRoomProgress aggregation (cleared stacks, …
rschlaefli Jul 10, 2026
f4b24d6
test(graphql): seed instanceStatistics in escape-room fixture so grad…
rschlaefli Jul 10, 2026
a2077ff
fix(escape-room): surface structured error codes as participant feedb…
rschlaefli Jul 10, 2026
e4fd0ed
feat(escape-room): time-penalty hints backend (Phase 3a)
rschlaefli Jul 10, 2026
778eb22
feat(manage): per-element escape-room hint authoring in activity wizard
rschlaefli Jul 10, 2026
fd4d6e1
docs(project): record Phase 3b commit hash in escape-room roadmap
rschlaefli Jul 10, 2026
f9bb84b
feat(pwa): participant request-hint button for escape-room stacks
rschlaefli Jul 10, 2026
773ede3
docs(project): mark Phase 3 (escape-room hints) complete in roadmap
rschlaefli Jul 10, 2026
69229ea
feat(escape-room): lecturer progress dashboard in activity evaluation
rschlaefli Jul 10, 2026
3977fad
docs(project): mark Phase 4 escape-room dashboard complete (1a291bff9)
rschlaefli Jul 10, 2026
c2e0a0b
feat(pwa): escape-room UX polish — completion stats, progress chip, u…
rschlaefli Jul 10, 2026
7fea18a
feat(escape-room): lecturer-authored intro story shown on start screen
rschlaefli Jul 10, 2026
045032a
docs(project): record UX polish + intro story slices and add playwrig…
rschlaefli Jul 10, 2026
59bc009
fix(escape-room): repair practice quiz game loop under server-side st…
rschlaefli Jul 10, 2026
cadccde
docs(project): record escape room game loop fix and microlearning ret…
rschlaefli Jul 10, 2026
9ac7439
test(escape-room): rewrite playwright e2e suite for the full escape r…
rschlaefli Jul 10, 2026
a5c8a18
docs(project): record playwright suite completion and wizard hint rou…
rschlaefli Jul 10, 2026
7729def
docs(apps/docs): add escape room lecturer and student tutorials
rschlaefli Jul 10, 2026
238d461
docs(wiki): log escape room production pass and mark docs/process-gua…
rschlaefli Jul 10, 2026
b8cecf5
docs(project): revise escape room production roadmap
rschlaefli Jul 11, 2026
85f74c3
docs(project): record clean escape room replay
rschlaefli Jul 11, 2026
533b0f6
fix(escape-room): derive preview authority on server
rschlaefli Jul 11, 2026
a431593
fix(group-activity): validate escape-room submissions atomically
rschlaefli Jul 11, 2026
589a6dc
fix(escape-room): authorize and restore revealed hints
rschlaefli Jul 11, 2026
d226386
fix(manage): preserve escape-room hints on edit
rschlaefli Jul 11, 2026
26617bd
fix(microlearning): support escape-room retries
rschlaefli Jul 11, 2026
beccffd
fix(escape-room): prevent duplicate attempt statistics
rschlaefli Jul 11, 2026
270640a
fix(escape-room): anchor countdown to server time
rschlaefli Jul 11, 2026
d6840f2
fix(manage): include all escape-room participants
rschlaefli Jul 11, 2026
0caa509
feat(group-activity): add escape-room participant flow
rschlaefli Jul 11, 2026
1ad9e27
feat(group-activity): add escape-room monitoring
rschlaefli Jul 11, 2026
18d2950
fix(live-quiz): enforce escape-room attempts
rschlaefli Jul 11, 2026
9931c1d
feat(live-quiz): add escape-room workflow
rschlaefli Jul 12, 2026
f90a4b8
feat(live-quiz): add escape-room monitoring
rschlaefli Jul 12, 2026
f4a387a
feat(elements): add QR scan contracts
rschlaefli Jul 12, 2026
f0c7548
feat(manage): add QR scan authoring
rschlaefli Jul 12, 2026
b7e914a
feat(manage): add QR escape-room print sheets
rschlaefli Jul 12, 2026
acdc029
feat(pwa): add QR scan escape-room answers
rschlaefli Jul 12, 2026
7b4aa67
fix(escape-room): allow microlearning retries and show overlay for wi…
rschlaefli Jul 12, 2026
78a7844
fix(manage): restrict QR-scan elements to escape-room activities
rschlaefli Jul 12, 2026
54ffad9
test(escape-room): verify complete workflow across quiz, microlearnin…
rschlaefli Jul 12, 2026
835c656
fix(escape-room): polish LiveQuiz escape blocks and tighten access gates
rschlaefli Jul 13, 2026
5a5afe1
fix(escape-room): resolve Playwright regressions and QR CSPRNG finding
rschlaefli Jul 13, 2026
75549e6
fix(graphql): escape-room progress roster tracks enrolled participant…
rschlaefli Jul 13, 2026
56b51e1
fix(graphql): restore escape-room block settings when editing a live …
rschlaefli Jul 13, 2026
45f2c29
fix(escape-room): enforce live quiz stage order
rschlaefli Jul 13, 2026
046b430
fix(escape-room): validate runtime resources
rschlaefli Jul 13, 2026
40a30d5
fix(escape-room): harden client state recovery
rschlaefli Jul 13, 2026
83cf9ed
docs(escape-room): align runtime contracts
rschlaefli Jul 13, 2026
d455b10
Merge remote-tracking branch 'origin/v3' into codex/escape-room-produ…
rschlaefli Jul 19, 2026
5c4a14d
fix(escape-room): preserve browser-safe shared types
rschlaefli Jul 19, 2026
5f7337c
fix(hatchet): keep development workers alive
rschlaefli Jul 19, 2026
61a0cbf
docs(solutions): document Hatchet watch crash
rschlaefli Jul 19, 2026
2d7c530
fix(escape-room): address review cleanup
rschlaefli Jul 19, 2026
7a3f30a
fix(prisma): create QR code index concurrently
rschlaefli Jul 19, 2026
80289b1
test(escape-room): verify complete workflow
rschlaefli Jul 19, 2026
39c873a
fix(escape-room): close release blockers
rschlaefli Jul 19, 2026
adf5eb9
docs(escape-room): synchronize release evidence
rschlaefli Jul 19, 2026
0865c55
chore(escape-room): integrate v3
rschlaefli Jul 19, 2026
31ea8b8
docs(project): record final escape room verification
rschlaefli Jul 19, 2026
b3f0825
refactor(response-api): remove redundant escape guard
rschlaefli Jul 19, 2026
4a30012
fix(shared-components): satisfy QR scan format check
rschlaefli Jul 19, 2026
4803a5a
test(graphql): avoid hard-coded escape credential
rschlaefli Jul 19, 2026
40e223e
test(playwright): follow server-driven escape advance
rschlaefli Jul 19, 2026
4bd6f8c
chore(escape-room): sync v3
rschlaefli Jul 26, 2026
351ca33
docs(project): record escape-room base sync
rschlaefli Jul 26, 2026
a731cc9
docs(project): correct escape-room next action
rschlaefli Jul 26, 2026
8a416b9
docs(project): record green escape-room CI
rschlaefli Jul 26, 2026
4be19aa
docs(project): align escape-room release verdict
rschlaefli Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/klicker-environment-doctor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ devrouter exec . -- tail -n 50 /tmp/dev.log

`devrouter doctor --repo .` provides static diagnostics. `devrouter ensure .` resolves the checkout-specific overlay and is the authoritative runtime proof.

If LiveQuiz submissions return HTTP 404 while the response API health check is green, verify that `NEXT_PUBLIC_ADD_RESPONSE_URL` ends in `/AddResponse`. The response API host root accepts GET health checks only; the PWA must POST to `/AddResponse` in primary, workspace, and plain-localhost modes.

### Path B: Host-based Setup

Manually boot the compose infrastructure:
Expand Down
3 changes: 3 additions & 0 deletions .agents/skills/klicker-playwright-e2e/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ volta run pnpm --filter @klicker-uzh/playwright exec playwright test --project=c
volta run pnpm --filter @klicker-uzh/playwright test -- --project=chromium
```

For a linked DevPod behind devrouter, run Playwright inside the exact application container and keep every URL on the same workspace suffix. The config accepts `PLAYWRIGHT_BROWSER_EXECUTABLE_PATH` when Chromium is installed outside Playwright's normal cache flow and `PLAYWRIGHT_HOST_RESOLVER_RULES` to map the namespaced `*.localhost` hosts to devrouter's `devnet` IP. Pass matching `URL_MANAGE`, `URL_STUDENT`, `URL_STUDENT_LOGIN`, `URL_API`, `URL_AUTH`, `APP_ORIGIN_AUTH`, and `COOKIE_DOMAIN` values; a primary-host redirect from a namespaced test is an environment mismatch, not a missing selector.

For live quiz answer submission, response processing, scheduled microlearnings, or Hatchet workflow failures, start the missing services explicitly:

```bash
Expand All @@ -76,6 +78,7 @@ Ensure the response processor is not running with `ASSESSMENT_MODE=true` when va
- Sudden Firefox/WebKit execution: Playwright is running all configured projects. Pass `--project=chromium` or keep non-Chromium projects commented in config if Chromium-only is desired.
- UI mode does not automatically mean tests execute; prefer CLI runs for verification and use UI mode only for interactive debugging.
- A passing `CLEANUP` followed by immediate failures often means frontend apps or auth URLs are unavailable after setup.
- A failure screenshot on `auth.klicker.localhost` during a namespaced run means the frontend was compiled without the workspace-specific public auth URL. Restart that exact app with the post-start environment before changing locators.

Before blaming a test, probe the apps:

Expand Down
9 changes: 6 additions & 3 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@ analytics image and lint CI so the root quality gate runs inside the container.
- `node_modules` is a named volume (pnpm hoists natives into the root
`node_modules/.pnpm`, so one root volume covers the monorepo).
- Reset the DB: `pnpm --filter @klicker-uzh/prisma exec prisma migrate reset --skip-seed --force`.
- `response-api` + both workers run `tsx --watch --env-file=.env`; node 24 errors
if `.env` is missing, so `post-create` seeds an **empty** `.env` in each dir
(the container env from `devcontainer.env` is what actually applies).
- `response-api` runs `tsx --watch`; the Hatchet workers build with Rollup and
run the emitted JavaScript under nodemon. Hatchet's heartbeat worker threads
are incompatible with in-process watch loaders. Node 24 errors if an
`--env-file` is missing, so `post-create` seeds an **empty** `.env` in each
directory (the container env from `devcontainer.env` is what actually
applies).
- Tier 3 (`chat`) needs an upstream LLM key: set `UPSTREAM_OPENAI_API_KEY`.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.env
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ NEXT_PUBLIC_MANAGE_URL=https://manage.klicker.localhost
NEXT_PUBLIC_PWA_URL=https://pwa.klicker.localhost
NEXT_PUBLIC_ASSESSMENT_URL=https://pwa.klicker.localhost
NEXT_PUBLIC_CONTROL_URL=https://control.klicker.localhost
NEXT_PUBLIC_ADD_RESPONSE_URL=https://response-api.klicker.localhost
NEXT_PUBLIC_ADD_RESPONSE_URL=https://response-api.klicker.localhost/AddResponse

# --- SSR app->app stays intra-container (bypasses devrouter, no cross-host TLS) ---
API_URL_SSR=http://localhost:3000/api/graphql
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/post-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ if [ ! -s /etc/devrouter/mkcert-rootCA.pem ]; then
export NEXT_PUBLIC_PWA_URL=http://localhost:3001
export NEXT_PUBLIC_ASSESSMENT_URL=http://localhost:3001
export NEXT_PUBLIC_CONTROL_URL=http://localhost:3003
export NEXT_PUBLIC_ADD_RESPONSE_URL=http://localhost:7078
export NEXT_PUBLIC_ADD_RESPONSE_URL=http://localhost:7078/AddResponse
export NEXT_PUBLIC_CHAT_URL=http://localhost:3004
export CORS_ALLOWED_ORIGINS=http://localhost:3001
export NODE_EXTRA_CA_CERTS=""
Expand All @@ -58,7 +58,7 @@ elif [ -n "${WORKSPACE:-}" ]; then
export NEXT_PUBLIC_PWA_URL=https://pwa.klicker.${WORKSPACE}.localhost
export NEXT_PUBLIC_ASSESSMENT_URL=https://pwa.klicker.${WORKSPACE}.localhost
export NEXT_PUBLIC_CONTROL_URL=https://control.klicker.${WORKSPACE}.localhost
export NEXT_PUBLIC_ADD_RESPONSE_URL=https://response-api.klicker.${WORKSPACE}.localhost
export NEXT_PUBLIC_ADD_RESPONSE_URL=https://response-api.klicker.${WORKSPACE}.localhost/AddResponse
export CORS_ALLOWED_ORIGINS=https://pwa.klicker.${WORKSPACE}.localhost
export AUTH_LECTURER_ALLOWED_HOSTS=manage.klicker.${WORKSPACE}.localhost,127.0.0.1:3002
export AUTH_STUDENT_ALLOWED_HOSTS=pwa.klicker.${WORKSPACE}.localhost,127.0.0.1:3001
Expand Down
8 changes: 8 additions & 0 deletions apps/analytics/prisma/schema/element.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum ElementType {
FLASHCARD
SELECTION
CASE_STUDY
QR_SCAN
}

model Element {
Expand All @@ -40,6 +41,10 @@ model Element {
status ElementStatus @default(READY)
type ElementType

// Opaque authoring/print secret for QR_SCAN elements. It is deliberately
// stored outside the participant-facing elementData JSON snapshot.
qrScanCode String? @unique

tags Tag[]

elementInstances ElementInstance[]
Expand Down Expand Up @@ -205,6 +210,9 @@ model ElementBlock {
liveQuizId String @db.Uuid
activeInLiveQuiz LiveQuiz[] @relation(name: "ActiveElementBlock")

escapeRoomConfig EscapeRoomConfig?
escapeRoomAttempts EscapeRoomAttempt[]

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

Expand Down
2 changes: 2 additions & 0 deletions apps/analytics/prisma/schema/participant.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ model Participant {
participantCourseAnalytics ParticipantCourseAnalytics[]
participantActivityPerformances ParticipantActivityPerformance[]
coursePerformances ParticipantPerformance[]
escapeRoomAttempts EscapeRoomAttempt[]
groupAssignmentPoolEntries GroupAssignmentPoolEntry[]
messages GroupMessage[]
chatThreads ChatThread[]
Expand All @@ -112,6 +113,7 @@ model ParticipantGroup {

participants Participant[]
groupActivities GroupActivityInstance[]
escapeRoomAttempts EscapeRoomAttempt[]
awards AwardEntry[]
achievements GroupAchievementInstance[]
messages GroupMessage[]
Expand Down
77 changes: 77 additions & 0 deletions apps/analytics/prisma/schema/quiz.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ model PracticeQuiz {
isAssessmentEnabled Boolean @default(false)
isDeleted Boolean @default(false)

escapeRoomConfig EscapeRoomConfig?
escapeRoomAttempts EscapeRoomAttempt[]

stacks ElementStack[]

responses QuestionResponse[]
Expand Down Expand Up @@ -167,6 +170,9 @@ model MicroLearning {
isAssessmentEnabled Boolean @default(false)
isDeleted Boolean @default(false)

escapeRoomConfig EscapeRoomConfig?
escapeRoomAttempts EscapeRoomAttempt[]

areInstancesOutdated Boolean @default(false) // flag to indicate if the any instances in this microlearning are outdated due to changes in the corresponding element

stacks ElementStack[]
Expand Down Expand Up @@ -277,6 +283,9 @@ model GroupActivity {
isAssessmentEnabled Boolean @default(false)
isDeleted Boolean @default(false)

escapeRoomConfig EscapeRoomConfig?
escapeRoomAttempts EscapeRoomAttempt[]

stacks ElementStack[]

parameters GroupActivityParameter[]
Expand Down Expand Up @@ -463,3 +472,71 @@ view UserActivities {
}

// #endregion

enum EscapeRoomStatus {
IN_PROGRESS
COMPLETED
EXPIRED
}

model EscapeRoomConfig {
id String @id @default(uuid()) @db.Uuid
timeLimit Int
hintPenalty Int @default(30)
lockoutSeconds Int @default(5)
introText String?

practiceQuiz PracticeQuiz? @relation(fields: [practiceQuizId], references: [id], onDelete: Cascade)
practiceQuizId String? @unique @db.Uuid

microLearning MicroLearning? @relation(fields: [microLearningId], references: [id], onDelete: Cascade)
microLearningId String? @unique @db.Uuid

groupActivity GroupActivity? @relation(fields: [groupActivityId], references: [id], onDelete: Cascade)
groupActivityId String? @unique @db.Uuid

elementBlock ElementBlock? @relation(fields: [elementBlockId], references: [id], onDelete: Cascade)
elementBlockId Int? @unique
}

model EscapeRoomAttempt {
id String @id @default(uuid()) @db.Uuid

startedAt DateTime @default(now())
timeLimit Int
penaltySeconds Int @default(0)
hintsUsed Json @default("[]") // Store used hint IDs/keys
status EscapeRoomStatus @default(IN_PROGRESS)
completedAt DateTime?
lockoutUntil DateTime?

// Set once the prune job has processed this finished attempt for retention.
// Response/instance statistics are owned by submission/event-time paths;
// completed/expired attempts remain the replay-integrity record until stale.
statsAggregatedAt DateTime?

// Individual Play (PracticeQuiz / MicroLearning / LiveQuiz block)
participant Participant? @relation(fields: [participantId], references: [id], onDelete: Cascade)
participantId String? @db.Uuid

// Group Play (GroupActivity)
group ParticipantGroup? @relation(fields: [groupId], references: [id], onDelete: Cascade)
groupId String? @db.Uuid

practiceQuiz PracticeQuiz? @relation(fields: [practiceQuizId], references: [id], onDelete: Cascade)
practiceQuizId String? @db.Uuid

microLearning MicroLearning? @relation(fields: [microLearningId], references: [id], onDelete: Cascade)
microLearningId String? @db.Uuid

groupActivity GroupActivity? @relation(fields: [groupActivityId], references: [id], onDelete: Cascade)
groupActivityId String? @db.Uuid

elementBlock ElementBlock? @relation(fields: [elementBlockId], references: [id], onDelete: Cascade)
elementBlockId Int?

@@unique([participantId, practiceQuizId])
@@unique([participantId, microLearningId])
@@unique([groupId, groupActivityId])
@@unique([participantId, elementBlockId])
}
41 changes: 41 additions & 0 deletions apps/docs/docs/student_tutorials/escape_room.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Escape Room
---

## What Is an Escape Room?

An Escape Room is a timed challenge available in four activity types: Practice Quiz, Microlearning, Group Activity, and Live Quiz. Practice Quiz and Microlearning present questions in ordered stacks. Live Quiz runs the challenge question by question inside a single block. Group Activity gives the whole group one shared challenge to complete together.

For a full overview of how lecturers set up Escape Rooms, see the [lecturer tutorial](/tutorials/escape_room).

## Before Starting

Students must be logged in and enrolled in the course before accessing an Escape Room. Group Activity Escape Rooms additionally require group membership. Live Quiz Escape Rooms require a regular participant account; temporary participation is not eligible.

The activity may display introductory text before the attempt begins; reading that introduction does not start the timer. The countdown begins only when the student (or, in a Group Activity, any group member) selects **Start Attempt**.

:::warning One attempt only – and the timer keeps running
Each logged-in participant has exactly one attempt in Practice Quiz, Microlearning, and Live Quiz Escape Room blocks. In a Group Activity, the group shares a single attempt. The timer continues across browser closes, connection losses, and page reloads; it does not pause or reset. Plan accordingly before selecting **Start Attempt**.
:::

## Working Through the Challenge

In Practice Quiz and Microlearning, questions are grouped into ordered stacks – a stack may contain more than one question, and the next stack unlocks only after every question in the current stack is answered correctly. In a Live Quiz Escape Room, questions progress one at a time within the block; the next question unlocks after the current answer is correct. In a Group Activity, the complete set of questions is visible from the start and all answers are submitted together when the group is ready.

A wrong submission does not reset progress – the current question or shared task stays open, and a short lockout applies before another attempt can be made.

If the lecturer added a hint to a question, a **Reveal hint** button appears. Selecting it deducts the corresponding number of seconds from the remaining time and displays the hint. Hints that have already been revealed are restored on reload.

Reloading restores server-recorded progress. The timer keeps running during a reload, so there is no benefit to refreshing mid-attempt.

## QR Scan Stages

Some stages require scanning a QR code at a physical station in the room. To scan, select **Scan QR code** and grant camera access when prompted. If the camera is unavailable or if permission is denied, select **Enter code manually** instead – each station has its own code printed beside its QR code. Decoy stations may appear in the room; only the code from the real station advances the stage. Entering a decoy station's code is treated as a wrong answer and triggers the same lockout period as any other incorrect submission.

## Group Activity: One Attempt, Shared by All

The Group Activity Escape Room gives every group member access to the full set of questions from the start. Answers, revealed hints, any lockout period, and completion are shared across the group. Any group member may submit the shared answer set when the group is ready. Group members cannot reset the shared attempt themselves; a lecturer reset restores access to the entire group.

## Completion and Results

After completing an Escape Room, the result screen shows the escape time, hints used, and total penalty. If time expires, staged modes additionally show which stages were cleared. Group Activity has no cleared-stage data; the result reports the shared challenge outcome instead. Results are final unless a lecturer resets the attempt.
Loading
Loading