Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/cypress-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:
- name: Define node version
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 24

- name: Check out repository code
uses: actions/checkout@v3
Expand Down Expand Up @@ -336,7 +336,7 @@ jobs:
- name: Define node version
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 24

- name: Check out repository code
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/playwright-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
- name: Define node version
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 24

- name: Check out repository code
uses: actions/checkout@v3
Expand Down
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Quick Reference

- **Monorepo**: pnpm 10.x + Turborepo, Node.js 20 (Volta-pinned)
- **Monorepo**: pnpm 10.x + Turborepo, Node.js 24 (Volta-pinned)
- **Main branch**: `v3`
- **Package names**: `@klicker-uzh/<name>` (e.g., `@klicker-uzh/graphql`)

Expand Down Expand Up @@ -197,11 +197,14 @@ Without Traefik, use `http://localhost:<port>` directly. The `*.klicker.com` dom

## Codebase Learnings

- **Participation access vs leaderboard activity**: `Participation.isActive` is used for active leaderboard/gamification state and should not be treated as the generic course-enrollment check. For course access gates, require the `Participation` row itself; only require `isActive` when awarding or displaying active leaderboard state. (`packages/graphql/src/services/`)
- **Prisma Decimal nullish check**: `Decimal` fields are objects, not numbers. `Decimal(0)` is truthy, so never use truthy checks for Decimal-to-number conversions -- always use `!= null`. (`packages/graphql/src/`)
- **Adaptive runtime estimates**: Adaptive `b`/`c` derivation and theta/information math live in `packages/adaptive-learning/src/index.ts`; live traversal and stop checks in `packages/graphql/src/services/adaptiveLearning.ts` compute subcompetence and competence estimates directly from their own response records, then aggregate the overall estimate from weighted competence estimates.
- **Chat app auth guard pattern**: Route handlers in `apps/chat/src/app/api/chatbots/` use a 3-step auth pattern: `getParticipantId` -> `getChatbotOr404` -> `requireParticipation`. The composed helper `withChatbotAuth(req, chatbotId)` in `apps/chat/src/lib/server/apiGuards.ts` handles the standard `{ courseId: true }` case. Use it for new routes; only fall back to individual guards when you need a custom chatbot `select`.
- **Feature flag guards**: Don't combine feature flags with data-dependent counts (e.g., `privatePreview && numChatbots > 0`). The flag alone should gate visibility; combining with counts creates chicken-and-egg problems.
- **Zustand store error handling**: Async actions in zustand stores must set fallback state in `catch` blocks, not just log. Otherwise the UI stays in loading/broken state on network errors. (`apps/chat/src/stores/`)
- **Test environment caveats**: `pnpm run test:run` triggers Cypress which needs a running DB + seeded data. `pnpm --filter @klicker-uzh/graphql test` needs `HATCHET_CLIENT_TOKEN`. For verifying non-DB changes locally, target specific packages (e.g., `pnpm --filter @klicker-uzh/grading test`, `pnpm --filter @klicker-uzh/util test`).
- **Prisma-data seed checks**: `packages/prisma-data` has no `check`/`build` script, and its broad tsconfig includes legacy migration scripts with unrelated type errors. For `seedTEST.ts` changes, use a targeted `pnpm exec tsc --noEmit --target es2022 --module NodeNext --moduleResolution NodeNext --esModuleInterop --skipLibCheck --strict --noUncheckedIndexedAccess --allowJs --resolveJsonModule packages/prisma-data/src/data/seedTEST.ts`.
- **PR review triage**: Copilot/CodeRabbit/SonarCloud flag many false positives. Always check if guards/fallbacks already exist before "fixing" reported issues. Confirm with the actual code, not the bot summary.
- **agent-browser via npx**: Always use `npx agent-browser` instead of bare `agent-browser`. Global install conflicts with Volta's Node shim and fails with "Could not execute command".
- **LTI launch target resolver contract**: Launch targets are resolved in strict precedence `custom claim (klicker_redirect_to)` -> `query redirectTo`; no env fallback is used in resolver logic. Validation fails closed on the first present invalid source and enforces URL hostname exact/subdomain checks against `COOKIE_DOMAIN` and `DF_DOMAIN` (never substring matching). (`apps/lti/src/launchTarget.ts`)
Expand All @@ -219,6 +222,9 @@ Without Traefik, use `http://localhost:<port>` directly. The `*.klicker.com` dom
- **Local embed harness target**: `util/embed-harness/` is for local verification only and should target the branch-local PWA (`http://127.0.0.1:3101/...`), not `https://pwa.klicker.com/...`, because production CSP / `frame-ancestors` blocks localhost embedding. (`util/embed-harness/`)
- **Chat PWA login redirects**: `apps/chat/src/app/noLogin/page.tsx` must pass an absolute chat URL to the PWA login `redirect_to`; a relative chatbot path makes the PWA redirect to its own domain and 404. Local chat dev also needs ignored local env values for the backend `APP_SECRET` and `DATABASE_URL` so participant cookies verify and Prisma can load chatbot data.
- **Chat Vitest alias resolution**: `apps/chat/vitest.config.ts` mirrors the app `@/*` alias from `apps/chat/tsconfig.json`; keep this in sync when adding client tests for modules that import from `@/src/...`.
- **Adaptive SE-stop reachability**: a 3PL item answered at its own difficulty carries information I = a²(1−c)/(4(1+c)), so the minimum reachable standard error after n items is 1/√(n·I); for 4-choice SC items (c=0.25) SE thresholds below ~1/√(0.34·n) can never fire and the stop silently degrades to the question cap. Validate any `standardErrorThreshold` against the item pool's guessing parameters. (`packages/adaptive-learning/`, `packages/graphql/src/services/adaptiveLearning.ts`; analysis in `project/2026-07-07-adaptive-learning-consolidated-review.md`)
- **Adaptive v2 direction**: Future adaptive-learning work should target cross-course reusable competence trees plus an `ADAPTIVE` practice quiz mode, not a standalone adaptive activity. The implementation plan is in `project/PLAN-adaptive-learning-v2.md`.
- **Node runtime pins**: Runtime bumps need to update the root Volta pin, per-package `engines.node`, and app Docker base images together; otherwise pnpm and container builds drift. (`package.json`, `apps/*/package.json`, `packages/*/package.json`, app `Dockerfile`s)

## Factory Skills (AI Assistance)

Expand Down
2 changes: 1 addition & 1 deletion apps/analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"script:qa": "../../util/_run_with_infisical.sh --env stg poetry run python -m"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/auth/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base

ARG NODE_ENV production
ENV NODE_ENV $NODE_ENV
Expand Down
2 changes: 1 addition & 1 deletion apps/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"start:test": "cross-env NODE_ENV=test pnpm start"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/backend-docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS deps
Expand Down
2 changes: 1 addition & 1 deletion apps/backend-docker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"start:test": "nyc --silent node -r dotenv/config dist/index.js"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/chat/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base

ARG NODE_ENV production
ENV NODE_ENV $NODE_ENV
Expand Down
2 changes: 1 addition & 1 deletion apps/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"test:watch": "vitest"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"write-translations": "docusaurus-write-translations"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-control/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS builder
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"start:test": "cross-env NODE_ENV=test pnpm start"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-manage/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS builder
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-manage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"start:test": "cross-env NODE_ENV=test pnpm start"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
28 changes: 18 additions & 10 deletions apps/frontend-manage/src/components/courses/CourseListButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { Badge, Button, Tooltip } from '@uzh-bf/design-system'
import dayjs from 'dayjs'
import { useTranslations } from 'next-intl'
import { Dispatch, SetStateAction, useState } from 'react'
import { Dispatch, KeyboardEvent, SetStateAction, useState } from 'react'
import { twMerge } from 'tailwind-merge'
import AssessmentBadge from '../activities/overview/AssessmentBadge'
import ActivityLogDialog from '../sharing/ActivityLogDialog'
Expand Down Expand Up @@ -73,19 +73,27 @@ function CourseListButton({
: false
const courseRunning = dayjs(course?.endDate).isAfter(dayjs())
const [activityLogOpen, setActivityLogOpen] = useState(false)
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onClick()
}
}

return (
<>
<Button
className={{
root: twMerge(
'flex w-full flex-row justify-between rounded-md border border-solid px-3 py-2 shadow-sm',
typeof course?.color !== 'undefined' && 'border-b-4!'
),
}}
<div
role="button"
tabIndex={0}
className={twMerge(
'focus-visible:outline-primary-80 flex w-full cursor-pointer flex-row justify-between rounded-md border border-solid px-3 py-2 text-left shadow-sm transition-colors hover:bg-slate-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
typeof course?.color !== 'undefined' && 'border-b-4!'
)}
style={{ borderBottomColor: course?.color }}
onClick={onClick}
data={data}
onKeyDown={handleKeyDown}
data-cy={data?.cy}
data-test={data?.test}
>
<div>
<div className="ml-1 flex flex-row items-center gap-3">
Expand Down Expand Up @@ -204,7 +212,7 @@ function CourseListButton({
) : null}
</div>
) : null}
</Button>
</div>

{course && activityLogOpen ? (
<ActivityLogDialog
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-pwa/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS builder
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-pwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"start:test": "cross-env NODE_ENV=test pnpm start"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
5 changes: 1 addition & 4 deletions apps/frontend-pwa/src/pages/course/[courseId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function CourseOverview({
const { data, loading, error } = useQuery(GetCourseOverviewDataDocument, {
variables: { courseId },
})

const participation = data?.getCourseOverviewData?.participation
const { data: dataLeaderboard, loading: loadingLeaderboard } = useQuery(
GetStudentCourseLeaderboardDocument,
{
Expand Down Expand Up @@ -105,8 +105,6 @@ function CourseOverview({
})

useEffect(() => {
const participation = data?.getCourseOverviewData?.participation

// if assessment is enabled, switch to the assessment results tab automatically
if (data?.getCourseOverviewData?.course?.isAssessmentEnabled) {
setSelectedTab('assessment-results')
Expand Down Expand Up @@ -142,7 +140,6 @@ function CourseOverview({
const {
course,
participant,
participation,
groupLeaderboard,
groupLeaderboardStatistics,
inRandomGroupPool,
Expand Down
2 changes: 1 addition & 1 deletion apps/hatchet-worker-general/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS deps
Expand Down
2 changes: 1 addition & 1 deletion apps/hatchet-worker-general/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"start:test": "node --env-file .env dist/index.js"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/hatchet-worker-response-processor/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS deps
Expand Down
2 changes: 1 addition & 1 deletion apps/hatchet-worker-response-processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"start:test": "node --env-file .env dist/index.js"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/lti/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS deps
Expand Down
2 changes: 1 addition & 1 deletion apps/lti/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"start": "node dist/index.js"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/office-addin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"validate:taskpane": "office-addin-manifest validate src/manifest-taskpane.xml"
},
"volta": {
"node": "20.18.2",
"node": "24.16.0",
"pnpm": "10.2.0"
},
"config": {
Expand Down
2 changes: 1 addition & 1 deletion apps/olat-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS deps
Expand Down
2 changes: 1 addition & 1 deletion apps/olat-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"validate:openapi": "redocly lint static/openapi.yaml"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion apps/olat-api/test/docker/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20
FROM node:24

# Install dependencies
RUN npm install -g pnpm
Expand Down
2 changes: 1 addition & 1 deletion apps/response-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/node:20.19.4-alpine AS base
FROM docker.io/library/node:24.16.0-alpine AS base


FROM base AS deps
Expand Down
2 changes: 1 addition & 1 deletion apps/response-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"start:test": "node --env-file=.env dist/index.js"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../../package.json"
Expand Down
2 changes: 1 addition & 1 deletion cypress/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"test:watch": "../util/_run_with_infisical.sh --env dev-cypress cypress open --watch"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"extends": "../package.json"
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@
"test:watch": "run-p test dev:test"
},
"engines": {
"node": "=20"
"node": "=24"
},
"volta": {
"node": "20.19.4",
"node": "24.16.0",
"pnpm": "10.15.0"
},
"packageManager": "pnpm@10.15.0"
Expand Down
36 changes: 36 additions & 0 deletions packages/adaptive-learning/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@klicker-uzh/adaptive-learning",
"version": "0.1.0",
"license": "AGPL-3.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"devDependencies": {
"@rollup/plugin-node-resolve": "~15.3.1",
"@rollup/plugin-typescript": "~12.1.4",
"@types/node": "^20.19.11",
"cross-env": "~7.0.3",
"npm-run-all": "~4.1.5",
"rollup": "~4.34.9",
"typescript": "~5.6.3",
"vitest": "~3.2.4"
},
"scripts": {
"build": "run-s build:ts",
"build:test": "pnpm run build",
"build:ts": "cross-env NODE_ENV=production rollup -c",
"check": "tsc --noEmit",
"dev": "cross-env NODE_ENV=development rollup -c --watch",
"test": "vitest run test/index.test.ts",
"test:simulation": "vitest run test/simulation.test.ts"
},
"engines": {
"node": "=24"
},
"volta": {
"extends": "../../package.json"
},
"type": "module"
}
Loading