From 36695a06f68a82ec64da06f29b220d2fc0d78c26 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 19 Jul 2026 14:39:00 +0200 Subject: [PATCH] fix: leerling in werken-modus kreeg abusievelijk readonly:true (v0.4.33-beta) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Roadmap-punt "de voortgangen werken niet" onderzocht (niet blind herbouwd - eerst gediagnosticeerd via de code en git-geschiedenis). - Gevonden: pupil.js zette sinds v0.3.15-beta (toen kijken/werken-modus werd toegevoegd) readonly:true in BEIDE takken van de modus-keuze - een kopieerfout, want de hele reden voor die commit was net dat "werken" de leerling wél moet laten werken. In "werken"-modus hoort er geen readonly-vlag te staan; "kijken" (blijft ongewijzigd) is de enige modus die dat nodig heeft. - Concreet effect: de twee widgets die opts.readonly daadwerkelijk lezen (letters.js/anchor.js) verborgen voor leerlingen in werken-modus onterecht hun eigen bewerk-gereedschap (woordenlijst- editor, opslaan/bibliotheek) - gereedschap dat een leerling in werken-modus wél hoort te zien. De kernoefening zelf (spellen, slepen) werd hierdoor niet geblokkeerd, dus dit verklaart niet de volledige klacht - overige widgets negeren opts.readonly toch al. - Tegelijk de stille `.catch(()=>{})` op het voortgang-loggen vervangen door `console.warn` bij een mislukte POST /my/progress: een mislukte log (verlopen sessie, weggehaalde toewijzing, servergolf) verdween eerder spoorloos, wat een "werkt niet"-klacht onmogelijk te onderzoeken maakte. - Nieuwe test/progress-logging.test.js dekt het serverpad dat nog helemaal geen tests had: leerling-eigen toewijzing wint van klasbrede, thuiskind-zonder-school valt terug op de gekoppelde ouder, geen-toewijzing geeft 400 zonder insert, niet-leerling geeft 403, ongeldige widgetId/widgetType geeft 400 - alle zes bevestigen dat de SQL/toewijzingslogica zelf klopt, wat het probleem verder vernauwt tot de nu twee gefixte punten hierboven of iets dat alleen in de echte omgeving van de gebruiker reproduceerbaar is. - Live geverifieerd: werken-modus toont nu het bewerk-gereedschap in letters.js (voorheen verborgen), kijken-modus blijft exact ongewijzigd (overlay + verborgen gereedschap). Volledige testsuite: 118/118. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ --- VERSION | 2 +- public/js/core.js | 2 +- public/js/pupil.js | 20 ++++++- test/progress-logging.test.js | 103 ++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 test/progress-logging.test.js diff --git a/VERSION b/VERSION index fdacb98..429af07 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.32-beta \ No newline at end of file +0.4.33-beta \ No newline at end of file diff --git a/public/js/core.js b/public/js/core.js index d16404b..16f881f 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.4.32-beta"; +const VERSION = "0.4.33-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/public/js/pupil.js b/public/js/pupil.js index 39077dd..fd9536a 100644 --- a/public/js/pupil.js +++ b/public/js/pupil.js @@ -53,9 +53,23 @@ function renderPupilWidgets(widgets, mode){ hebben nog geen wid - dan is positie in de lijst de terugval-identiteit, stabiel zolang de leerkracht de widgets op het bord niet herschikt */ const widgetId = w.wid || (def.id + ":" + idx); - /* in kijken-modus geen voortgang: de leerling doet zelf niets */ - const opts = watch ? { readonly:true } : { readonly:true, onProgress: (p)=>{ - api("/my/progress", { method:"POST", body: { widgetId, widgetType: def.id, ...p } }).catch(()=>{}); + /* in kijken-modus geen voortgang: de leerling doet zelf niets. In + werken-modus moet de leerling juist wél kunnen werken - readonly:true + stond hier sinds v0.3.15-beta abusievelijk ook in de werken-tak (een + kopieerfout toen kijken/werken werd toegevoegd). De twee widgets die + opts.readonly echt lezen (letters.js/anchor.js) gebruiken het alleen om + hun eigen bewerk-gereedschap (woordenlijst-editor, opslaan/bibliotheek) + te verbergen, niet om de kernoefening te blokkeren - maar dat gereedschap + hoort een leerling in werken-modus wél te zien, dus dit was hoe dan ook + verkeerd om, los van de vraag of dit "voortgang werkt niet" verklaart. */ + const opts = watch ? { readonly:true } : { onProgress: (p)=>{ + /* eerder een stille .catch(()=>{}) - een mislukte log (verlopen sessie, + geen toewijzing meer, servergolf) verdween daarmee spoorloos, wat een + "voortgang werkt niet"-klacht onmogelijk te onderzoeken maakt. Nu + zichtbaar in de console (geen storende UI voor de leerling, wél een + spoor voor wie het probleem moet natrekken). */ + api("/my/progress", { method:"POST", body: { widgetId, widgetType: def.id, ...p } }) + .catch(e=>console.warn("voortgang loggen mislukt:", e.message)); }}; def.mount(body, w.state || null, opts); }); diff --git a/test/progress-logging.test.js b/test/progress-logging.test.js new file mode 100644 index 0000000..9306cd5 --- /dev/null +++ b/test/progress-logging.test.js @@ -0,0 +1,103 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import Fastify from 'fastify'; +import cookie from '@fastify/cookie'; +import rateLimit from '@fastify/rate-limit'; +import api from '../src/api.js'; + +/* "de voortgangen werken niet" (gebruikersklacht) - dit pad had nog GEEN enkele + test (mocked-pool of anders), ondanks dat het de kern van de voortgangs- + feature is: een leerling logt een afgerond moment, en het datamodel bepaalt + zelf teacher_id/board_id uit de toewijzing (nooit van de client). Dekt de + drie manieren waarop een toewijzing gevonden kan worden (leerling-eigen, + klasbreed, thuiskind-via-ouder) plus de foutpaden. */ +function makeApp(user, { assignmentRows = {}, parentRows = [] } = {}) { + const calls = []; + const query = async (sql, params = []) => { + calls.push({ sql, params }); + if (sql.includes('FROM sessions s JOIN users u')) return { rows: [user] }; + if (sql.includes('FROM user_roles')) return { rows: [] }; + if (sql === 'SELECT * FROM assignments WHERE pupil_id = $1') return { rows: assignmentRows.byPupil ? [assignmentRows.byPupil] : [] }; + if (sql === 'SELECT * FROM assignments WHERE class_id = $1') return { rows: assignmentRows.byClass ? [assignmentRows.byClass] : [] }; + if (sql.startsWith('SELECT parent_id FROM parent_children')) return { rows: parentRows }; + if (sql.startsWith('INSERT INTO progress_events')) return { rows: [] }; + return { rows: [] }; + }; + const pool = { query, async connect() { return { query, release() {} }; } }; + return (async () => { + const app = Fastify({ trustProxy: 2 }); + await app.register(cookie); + await app.register(rateLimit, { global: false }); + app.decorate('pg', pool); + await app.register(api, { prefix: '/api' }); + await app.ready(); + return { app, calls }; + })(); +} + +const cookies = { teach_session: 'x'.repeat(64) }; +const validBody = { widgetId: 'letters:0', widgetType: 'letters', attempts: 1, correct: 1, stars: 1 }; +function insertCall(calls) { + return calls.find(c => c.sql.startsWith('INSERT INTO progress_events')); +} + +test('voortgang: leerling-eigen toewijzing wint en levert de juiste teacher_id/board_id op', async () => { + const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 }; + const { app, calls } = await makeApp(pupil, { + assignmentRows: { + byPupil: { teacher_id: 3, board_id: 'bord-a' }, + byClass: { teacher_id: 4, board_id: 'bord-b' }, // zou genegeerd moeten worden + }, + }); + const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody }); + assert.equal(res.statusCode, 200, res.body); + const ins = insertCall(calls); + assert.ok(ins, 'INSERT verwacht'); + assert.equal(ins.params[1], 3); + assert.equal(ins.params[2], 'bord-a'); +}); + +test('voortgang: valt terug op de klasbrede toewijzing als er geen eigen toewijzing is', async () => { + const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 }; + const { app, calls } = await makeApp(pupil, { + assignmentRows: { byClass: { teacher_id: 4, board_id: 'bord-b' } }, + }); + const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody }); + assert.equal(res.statusCode, 200, res.body); + const ins = insertCall(calls); + assert.equal(ins.params[1], 4); + assert.equal(ins.params[2], 'bord-b'); +}); + +test('voortgang: thuiskind zonder school valt terug op de gekoppelde ouder als "leerkracht"', async () => { + const homeChild = { id: 11, username: 'timo', role: 'pupil', school_id: null, class_id: null, data: {}, data_rev: 0 }; + const { app, calls } = await makeApp(homeChild, { parentRows: [{ parent_id: 7 }] }); + const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody }); + assert.equal(res.statusCode, 200, res.body); + const ins = insertCall(calls); + assert.equal(ins.params[1], 7); + assert.equal(ins.params[2], 'thuis'); +}); + +test('voortgang: leerling op school zonder enige toewijzing krijgt 400, geen stille silent-fail insert', async () => { + const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 }; + const { app, calls } = await makeApp(pupil, {}); + const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody }); + assert.equal(res.statusCode, 400, res.body); + assert.ok(!insertCall(calls), 'er mag geen INSERT gebeurd zijn'); +}); + +test('voortgang: niet-leerling (bv. leerkracht) krijgt 403', async () => { + const teacher = { id: 3, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 }; + const { app } = await makeApp(teacher, {}); + const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody }); + assert.equal(res.statusCode, 403, res.body); +}); + +test('voortgang: ontbrekende widgetId/widgetType levert 400 op, niet een kapotte insert', async () => { + const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 }; + const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } }); + const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { attempts: 1, correct: 1 } }); + assert.equal(res.statusCode, 400, res.body); + assert.ok(!insertCall(calls)); +});