Release Notes
For the complete, commit-level history see CHANGELOG.md in the repository.
v3.3.1 — 2026-08-01
Accessibility, performance, and maintenance release. The headline fix is that answer grading — the single most important thing the app tells a student — was communicated by fill color alone and was never announced to assistive technology. No configuration or upgrade steps — a drop-in bump from 3.3.0.
Accessibility
- Answer grading is perceivable without color or sight —
checkAnswerspainted each result cell green or red and did nothing else, so a screen-reader user received no result at all and a colorblind user was left comparing two fills measuring 1.36:1 against each other, far below the 3:1 WCAG 2.1 AA non-text minimum and a 1.4.1 (Use of Color) failure. Result cells now carry✓ correct/✗ try againtext, and the status cell is arole="status"/aria-live="polite"region announcingN of M correct. - Controls have accessible names — answer inputs gained
aria-label="Return value for <invocation>", the like/dislike buttons gained labels with their images marked decorative, and the sign-in modal's close control (aspanwith anonClick, unreachable by keyboard) became a realbutton. - Text colors pass AA — the pass/fail colors met contrast as fills but not as text, so
rb-correct-text(#3E862E) andrb-wrong-text(#ED0000) were added as separate text tokens andrb-headerwas darkened to#337E9C. Re-toning the tokens alone would have changed nothing:HEADER_COLORwas emitted as an inline style on the same elements, so the inline value won. That constant and all 13 call sites are gone. - Link hover no longer turns red, which collided with the wrongness signal — it is now an underline.
- Spinners that were never visible now work — the like/dislike and admin spinners used
fa-spin, but Font Awesome is not loaded on those pages. Replaced with a CSS.rb-spinnerthat honorsprefers-reduced-motion. - The page wordmark is now an
h1, the language nav carriesaria-label="Languages", and pages emit<meta charset>and a viewport tag.
Performance
- Images got much lighter — the four like/dislike PNGs were 1600px wide and render at 30px (345 KB → 12 KB), and
nervous/panicmoved to JPEG (~450 KB → ~72 KB). - All six
imgsites gained explicitwidth/height, removing layout shift, plusloadinghints.
Changed
- The generated stylesheet can no longer drift unnoticed —
static/tailwind.cssis checked in, but the Gradle wiring only regenerates it on macOS, so the accessibility pass left it stale, still carrying CSS for classes nothing emits. It has been rebuilt (80,540 → 78,905 bytes), and a newTailwind CSSworkflow now regenerates it in CI and fails if the committed artifact does not match. - A design-system record —
PRODUCT.md,DESIGN.md, and.impeccable/design.jsoncapture the product context and the design system the accessibility work was audited against. - Documentation accuracy —
make dbresetwas referenced in four places but has never existed; all now name the real Flyway targets (dbmigrate,dbclean,dbinfo,dbvalidate).CLAUDE.mdalso shed 57 lines of content derivable from the codebase itself. - The versions plugin is applied by its catalog id rather than a hardcoded legacy string, clearing a Gradle deprecation warning.
Dependencies
Ktor 3.5.1 → 3.5.2 · Flyway 13.0.0 → 13.1.0 · common-utils 3.2.1 → 3.2.2 · versions plugin 0.54.0 → 0.57.0 · zensical 3.10.2 → 3.10.3 · markdown 0.0.51 → 0.0.52
v3.3.0 — 2026-07-26
Modernization + maintenance release. Adopts Kotlin's experimental collection-literal syntax across the codebase, refreshes dependencies (with major bumps to Flyway 13, prometheus-proxy 4.0, and common-utils 3.x), and lands a handful of code-quality cleanups. No configuration or upgrade steps — a drop-in bump from 3.2.1.
Changed
- Kotlin collection literals — enabled the experimental
-Xcollection-literalscompiler flag and converted 191 call sites acrosssrcandtest:emptyList()→[],listOf(...)→[...], andmutableListOf(...)→[...], the last with an explicitMutableList<T>type on the declaration so mutability is never inferred away. The flag scopes to project sources only — the JSR-223 engine that evaluates content DSL files at runtime is unaffected, so DSL content keeps usinglistOf()/mutableListOf(). - Build-script structure — the Dokka configuration moved into a root-level
configureDokka()helper, andconfigureVersions()moved into anallprojects {}block. - Code-quality cleanups — removed redundant imports of symbols defined within a file's own object/companion (five files), and simplified an early-return conditional in
Intercepts.isBrowsableContentPath(if (x) return true; return y→return x || y, behavior-preserving).
Fixed
- Dokka is warning-free — the unresolved
[initProperties]KDoc link inContentDsl.ktreferenced aPropertycompanion member that was not in scope, so it rendered as plain text. It now uses the fully-qualified custom-link-text form.
Dependencies
Kotlin 2.4.0 → 2.4.10 · common-utils 2.9.3 → 3.2.1 · prometheus-proxy 3.2.0 → 4.0.0 · Flyway 12.10.0 → 13.0.0 · Kotest 6.2.1 → 6.2.3 · Logback 1.5.18 → 1.5.38 · PostgreSQL driver 42.7.12 → 42.7.13 · Cloud SQL socket factory 1.28.6 → 1.29.0 · Kotlinter 5.5.0 → 5.6.0 · Kover 0.9.8 → 0.9.9
v3.2.1 — 2026-07-03
Maintenance release: a Gradle 9.6.1 upgrade, a routine dependency refresh, and two small build/test polish items. No functional changes to the running server and no upgrade steps — a drop-in bump from 3.2.0.
Changed
- Gradle 9.6.1 — the wrapper moves from 9.5.1 to 9.6.1.
- Smarter pre-release version filter —
configureVersions()rejects a pre-release candidate only when the current dependency is stable, so libraries tracked on a pre-release line (e.g. the detekt 2.0 alpha) keep surfacing newer pre-releases. ThedependencyUpdatestask is also marked incompatible with the configuration cache. TestSupport.forEachAnswertakes asuspendblock — matching the suspendfunctionInfo()call site introduced in 3.2.0.
Dependencies
Gradle 9.5.1 → 9.6.1 · common-utils 2.9.2 → 2.9.3 · Ktor 3.5.0 → 3.5.1 · Exposed 1.3.0 → 1.3.1 · Kotest 6.2.0 → 6.2.1 · Flyway 12.8.1 → 12.10.0 · PostgreSQL driver 42.7.11 → 42.7.12 · Cloud SQL socket factory 1.28.4 → 1.28.6 · Playwright 1.60.0 → 1.61.0 · detekt 2.0.0-alpha.4 → 2.0.0-alpha.5 · maven-publish 0.36.0 → 0.37.0
v3.2.0 — 2026-06-15
Security-hardening release. A multi-agent security review surfaced 48 confirmed findings (7 high, 19 medium, 22 low); all 48 are addressed here, alongside a batch of WebSocket/caching reliability fixes and the 3.1.9-era build/tooling cleanup.
Upgrade note — SESSION_SECRET is now required in production
Session cookies are now signed and encrypted, so the server will not start
in production without a SESSION_SECRET. Generate one with
openssl rand -hex 32, set it via the SESSION_SECRET environment variable,
use the same value on every node, and note that rotating it invalidates
all existing sessions. See Configuration › Secrets.
Security
- Signed + encrypted session cookies (
SessionTransportTransformerEncrypt, AES-128 + HMAC-SHA256). The cookies were previously unsigned plaintext, allowing theuserIdto be forged for a trivial authentication bypass and admin takeover. - Server-side code injection (RCE) removed — answer checking no longer evaluates the user's response as a script; list/array answers are parsed and compared directly.
- Teacher IDOR closed — class-management actions verify class ownership.
- XSS fixes — student answers over WebSockets are HTML-escaped; user-controlled names in
confirm()/onSubmithandlers are escaped. - OAuth hardening — Google and GitHub logins require a verified email; login rotates the browser session to prevent fixation.
- Rate limiting enforced per-IP; secrets masked in logs; the operational logging WebSocket requires admin.
Reliability
- WebSockets — fixed pinger concurrent-modification crashes, removed a shared dispatcher that blocked all clients, collapsed N+1 blocking JDBC, and made the answer dashboard drop-oldest under backpressure.
- Caching — geo cache short-circuits the DB, drops its global mutex, stops caching failures permanently, and is size-bounded; a dir-contents cache key mismatch is fixed; per-user answer channels are bounded.
- Parsing & startup — nested-brace Kotlin script conversion, Java invocation ordering, quote-aware Python list parsing, a non-numeric env-var startup crash, and a bounded answer-check loop.
Challenge.functionInfo()is nowsuspend— the blocking script eval runs onDispatchers.IOinstead of bridging throughrunBlockinginside request/WS coroutines.
Dependencies
common-utils → 2.9.2 · Kotlin 2.4.0 · Ktor 3.5.0 · Exposed 1.3.0 · Kotest 6.2.0 · detekt 2.0.0-alpha.4 · HikariCP 7.1.0 · prometheus-proxy 3.2.0