Gewijzigde functionaliteiten:
- Letterblokken logt per afgerond woord hoeveel pogingen het kostte en welke
letter op welke plek fout ging (beide invoerwijzen: blokken slepen en typen).
- Ballonnenwoord logt nu ook een verloren woord; dat leverde tot nu toe geen
enkele voortgang op. Een gewonnen woord telt de fout geraden letters mee.
- Flitswoorden stuurt de bestaande zelfcheck ("Gelukt" / "Nog oefenen") door
naar de voortgang; die uitkomst werd eerder weggegooid.
- Hakken & plakken krijgt dezelfde zelfcheck en daarmee als laatste taalwidget
een goed/fout-moment; de oefening zelf verandert niet.
- Voortgang-tab toont de fouten van taal: naast fout afgesloten opdrachten ook
"na N pogingen goed" (letterblokken kent geen faalmoment) en zelfchecks.
- Reeksstappen met flitswoorden of hakken & plakken wachten nu op een echt
afgerond moment in plaats van meteen te completeren.
Tests: 192 geslaagd (7 nieuw), plus live nagelopen in de leerlingweergave met
echte interactie per widget en een screenshot van de Voortgang-tab.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126hjBPD2ggZ2LEoDYZSW7S
- Bug gevonden en bevestigd (niet blind aangenomen): math.js riep
onProgress maar op ÉÉN plek aan, in endRound(), NA een heel blok van
10 sommen, met alleen het geaggregeerde totaal. Eén sessie van 10
sommen - "een groep opdrachten" - leverde dus precies 1 rij op in
progress_events, niet 10, en maakte "welke som ging fout" onmogelijk
te zien (alleen een totaaltelling per blok).
- Fix: onProgress verplaatst naar check(), bij elke afgeronde som (goed
op de eerste keer, of opgeven na 3 pogingen) - een sessie van 10
sommen levert nu 10 progress_events-rijen op, elk met een nieuw
optioneel detail-veld (welke som, gegeven antwoord, juist antwoord).
letters.js/ball.js blijven deze ronde ongemoeid (hun granulariteit
veroorzaakte niet deze bug).
- Nieuwe kolom progress_events.detail (JSONB, nullable, puur additief -
db/020_progress_detail.sql). POST /my/progress accepteert het
optioneel; te groot/geen plain object wordt stilzwijgend genegeerd
zonder de rest van de log-poging te laten falen (detail is decoratief,
geen kernfunctionaliteit).
- Voortgang-tab (admin.js) herontworpen, zelfde ontwerptaal als de
eerdere School-navigator-herbouw: vakgebied-tabs, een
klassenmanagement-infobadge (Groep {n} · Niveau {n}, opgehaald via de
bestaande /levels/pupil/:id-route) zodat resultaten meteen tegen het
toegewezen niveau afgezet kunnen worden, een met de hand getekende
SVG-nauwkeurigheidsring + staafdiagram (goed/fout per dag, laatste 14
dagen - geen library, zelfde aanpak als avatar.js) en een "recente
fouten"-lijst die precies toont welke som fout ging. De bestaande
ruwe tijdlijn blijft eronder staan, niets is verwijderd.
- Terzijde gevonden en gefixt: .am-btn.active bestond nergens in de
CSS, waardoor de vakgebied-tabs (hier én in Klassenmanagement,
v0.4.34-beta) geen enkele visuele aanduiding hadden welke actief was.
- Tests: test/progress-logging.test.js uitgebreid met 4 tests voor het
detail-veld (opgeslagen, te groot genegeerd, geen plain object
genegeerd, afwezig blijft null). Volledige testsuite: 132/132.
- Live geverifieerd: een math.js-sessie met een mix van goede/foute
sommen doorlopen en bevestigd dat er 3 aparte POST /my/progress-
aanroepen gebeuren voor 3 sommen (niet 1 aggregaat), elk met het
juiste detail; de herbouwde Voortgang-tab geopend met gemockte data
en met screenshots bevestigd dat de nauwkeurigheidsring (38% = 3/8,
rekenkundig geverifieerd), het klassenmanagement-infobadge, de
staafdiagram en de "recente fouten"-lijst (met de daadwerkelijke som)
correct tonen, én dat de active-tab-fix ook op de Klassenmanagement-
tab zichtbaar is.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
- Grootste openstaande roadmap-punt. Nieuwe tabel pupil_levels
(db/019_pupil_levels.sql, zelfde class_id/pupil_id-vorm als
assignments) legt per vakgebied (taal/rekenen/world) een niveaugroep
vast (groep 1-8, gebaseerd op de leerjaren van de basisschool waaraan
de leerdoelen gekoppeld zijn) + niveau (1-3, fijnmazige verfijning
BINNEN die groep). Klas-standaard met individuele leerling-overrides,
net zo laagdrempelig instelbaar als de standaard - het speciaal-
onderwijs-geval waar dat vanaf het begin de norm is.
- Server: GET/PUT/DELETE /levels/class/:id en /levels/pupil/:id
(src/api.js) hergebruiken exact het assignments-patroon (upsert via
partial-unique-index, pupil-eerst-dan-klas-resolutie), plus
GET /my/level voor de leerling-kant. Zelfde permissie
(assignments.manage) en sameSchool/teacherOnly/pupilAccessible-
guards als de bestaande toewijzingsroutes.
- Nieuwe admin-tab "Klassenmanagement": klas + vakgebied kiezen, een
klas-standaard instellen, en per leerling de effectieve groep/niveau
zien (overerft van de klas tenzij een eigen afwijking is ingesteld)
met bewerk- en terug-naar-standaard-knoppen.
- ECHTE inhoudskoppeling, niet alleen een standaardwaarde: rekenen is
het eerste vakgebied waarvan de SLO/TULE-leerlijn direct in
getalgrenzen te vertalen is (groep 3 t/m 20, groep 4 t/m 100, groep 5
t/m 1000, groep 6 t/m 10.000). Nieuwe SLO_MATH_RANGES/mathRangeFor()
in data.js; math.js's gen() gebruikt dit voor de daadwerkelijke
getallen zodra een leerling een groep heeft (S.yearGroup), met het
bestaande niveau 1-3 als verfijning daarbinnen. Zonder groep blijft
het oude vaste 10/20/100-gedrag exact bestaan.
- pupil.js geeft groep/niveau door aan taal/rekenen-widgets bij het
mounten: niveau is een standaardwaarde (een al opgeslagen bord-niveau
blijft leidend), groep wordt voor rekenen-widgets altijd meegegeven
(er is geen "oud" groep-concept om te respecteren). Terzijde ook de
stille .catch() op de nieuwe async renderPupilWidgets() opgelost.
- Taal/wereldoriëntatie krijgen deze ronde nog geen eigen
inhoudskoppeling (vraagt een eigen contentbron per groep, nog niet
uitgezocht) - eerlijk benoemd als vervolgstap, geen loze belofte.
- Tests: test/pupil-levels.test.js (10 tests) dekt de server-routes
(mocked-pool-patroon) én - via dezelfde vm-techniek als elders in
test/widgets.test.js voor MONEY_LEVELS - een ECHTE (niet regex-)test
van mathRangeFor die bevestigt dat elke groep de juiste getalgrens
oplevert en niveau nooit boven de groep-grens uitkomt. Twee bugs
gevonden en gefixt tijdens het schrijven van deze tests: null-yearGroup
werd per ongeluk als groep 1 behandeld i.p.v. "geen groep", en de
testrol had geen klas-eigenaarschap waardoor 403 in plaats van 200
terugkwam. Volledige testsuite: 128/128.
- Live geverifieerd in de sandbox: klas-standaard + leerling-override
instellen in de nieuwe admin-tab, en drie scenario's in de
leerling-omgeving die bevestigen dat de daadwerkelijk gegenereerde
sommen binnen de juiste groep-grens blijven (geen klassenmanagement →
ongewijzigd oud gedrag; groep zonder eigen bord-niveau → volledige
groep-grens; groep mét een al opgeslagen bord-niveau → dat niveau
blijft leidend, geschaald naar de juiste groep-grens).
- public/js/data.js bevatte al niet-gerelateerde, nog niet gecommitte
wijzigingen van vóór deze sessie (extra taalthema's/woorden) - alleen
de SLO_MATH_RANGES/mathRangeFor-hunk is hier meegenomen (git add -p),
de rest blijft bewust ongemoeid staan, net als test/language-library.test.js.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
- Nieuwe sectie "Exporteren per klas" in de bestaande Voortgang-tab: klas
kiezen + "Exporteer als CSV" downloadt de volledige activiteitenlijst
van die klas (leerling, widget, pogingen, goed, sterren, datum/tijd).
- Bouwt voort op de bestaande route GET /progress/class/:id, die tot nu
toe geen enkele frontend-consument had.
- Server: nieuwe optionele ?limit=-queryparameter (begrensd op 5000) -
de export vraagt expliciet meer dan de standaard activiteitenweergave
(200) op, zodat een drukke klas niet stilzwijgend wordt afgekapt in het
exportbestand. Zonder de parameter blijft het bestaande gedrag (200)
ongewijzigd.
- Eerste CSV-export in deze codebase: kleine lokale csvValue()/
downloadCsv()-helpers in admin.js (komma/quote/newline-escaping,
BOM zodat Excel-NL accenten en komma-scheiding herkent) - bewust nog
niet gedeeld/verplaatst naar core.js, want er is nu nog maar één
gebruiker (de geplande wachtwoorden-export kan dit later hergebruiken).
- Tests: test/progress-export.test.js (limit wordt doorgegeven, te hoge
waarde wordt afgekapt op 5000, standaardgedrag zonder parameter blijft
200) + volledige testsuite (112/112). Ook live geverifieerd: klas
kiezen zonder selectie toont een hint i.p.v. te crashen, een echte
CSV-download met correcte escaping van komma's/aanhalingstekens in
een leerlingnaam, en de vertaalde widget-namen in de kolommen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
- Nieuwe hoofdaccessoires: zonnebril, koptelefoon, feesthoedje.
- Nieuwe lichaamsaccessoires: ketting, rugtas (bandjes over de
schouders, zichtbaar van voren).
- Server-validatie (src/api.js) en vertalingen (nl+en) meegewerkt met
dezelfde nieuwe waarden - anders zou de client een optie tonen die de
server als "ongeldig hoofdaccessoire" afwijst.
- Geen wijziging aan de bestaande pet/kroon-onderdrukkingsregel nodig:
zonnebril/koptelefoon/feesthoedje bedekken het bovenhoofd niet volledig,
dus haar blijft er gewoon zichtbaar naast/onder.
- Geverifieerd: alle select-opties komen aan in de instellingen-editor,
een PUT met de nieuwe waarden slaagt (bestaande server-validatie
accepteert ze nu), en alle soort/accessoire-combinaties oogden goed in
screenshot (zonnebril op de dino, koptelefoon op het monster,
feesthoedje + ketting samen, rugtas op het dier).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
- Nieuwe migratie db/018_user_avatar.sql: kolom avatar_data (jsonb) op users.
- publicUser() geeft avatar overal automatisch mee (login, /auth/me,
/admin/users) - bestaande school/klas-rechten regelen vanzelf wie welke
personages ziet, geen nieuwe permissie-sleutel nodig.
- Nieuwe route PUT /me/avatar (open voor elke ingelogde rol, zelfde patroon
als /me/data): valideert soort/kleuren/haar/accessoires, slaat de eigen rij op.
- Nieuwe gedeelde module public/js/avatar.js: personage samenstellen uit
soort (dino/monster/dier - elk met een vast eigen lichaamsdeel, ook de
gevraagde dinostekels), lichaams- en haarkleur, haarstijl en hoofd-/
lichaamsaccessoires; gedeelde editor-UI voor zowel de instellingen-sectie
"Mijn personage" als de nieuwe widget.
- Instellingen-sectie "Mijn personage" (cms.js-patroon) - de leerling-toegang,
want widgets van categorie "class" zijn niet toewijsbaar in het
afgeschermde leerling-speelscherm.
- Widget 🦖 toegevoegd aan het bord (categorie "class") voor staf en
volledige-whiteboard-gebruikers.
- School-navigator (admin.js): leerkracht/beheerder ziet nu het personage
van elk klaslid in de personenlijst i.p.v. altijd de initialen-cirkel.
- Voorbereiding gerelateerd roadmappunt: math.js toont het eigen personage
kort naast "Goed!" als aanmoediging (pilot in één widget, geen nieuw
feedbacktype); zonder ingesteld personage blijft het gedrag ongewijzigd.
- Vertalingen (nl+en) voor de nieuwe widget, instellingen-sectie en editor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
- db/017: de class_teachers-trigger uit migratie 006 stond alleen 'teacher'
toe en blokkeerde daarmee (met een verwarrende "groepsleiding en klas..."
foutmelding) wat de applicatielaag allang toestond voor schoolbeheerder en
systeemmanager - de trigger is nooit meegegroeid toen die rollen later ook
aan een klas gekoppeld mochten worden. Nu ook admin/super toegestaan,
super school-onafhankelijk zoals de rest van de app dat al behandelt.
- rolwijziging kon een bestaande groepsleiding/schoolbeheerder via een simpel
dropdown-veld naar systeemmanager promoveren, zonder enige bevestiging -
in tegenstelling tot de nieuwe systeemmanager-aanmaak (met wachtwoord-
bevestiging). 'super' is nu geen geldig doel meer voor PATCH .../users/:id
(ook niet via het extraRoles-mechanisme); het rolwijzigingsveld zelf toont
zich niet meer voor een bestaande systeemmanager. Alleen de aparte
aanmaakstap (met bevestiging) kan nog een systeemmanager opleveren.
- meer verticale ruimte tussen rijen in het beheerpaneel (was vrijwel 0),
zodat je niet per ongeluk de verkeerde rij raakt
- de "+ ..."-toevoegknoppen staan nu rechts uitgelijnd i.p.v. links, zodat ze
duidelijker als aparte actie ogen i.p.v. als onderdeel van de lijst
Geverifieerd in een echte headless Chromium: rolwijziging toont voor een
groepsleiding alleen nog teacher/admin als opties.
- het aanmaken van een systeemmanager stond onder Scholen, los van de rest
van het gebruikersbeheer - verplaatst naar de Gebruikers-tab (bij "alle
scholen", naast de bestaande systeemmanagers-groep)
- extra veiligheidsstap: de aanmaker moet het eigen wachtwoord invullen ter
bevestiging vóórdat een nieuwe systeemmanager wordt aangemaakt - server
verifieert dit (POST /admin/users), niet alleen de client
- wie dit al mocht (alleen systeemmanagers, via creatableRoles) blijft
ongewijzigd - de bevestigingsstap is een aanvullende, geen vervangende,
controle
De echte, uiteindelijke oorzaak achter elke "serverfout" die niet door een
route-eigen foutafhandeling kwam (klas verwijderen, en zeer waarschijnlijk
ook de oorspronkelijke gebruiker-verwijderen-bug): de client stuurde bij élk
verzoek altijd "Content-Type: application/json" mee, ook bij een DELETE
zonder body. Fastify's standaard JSON-parser weigert zo'n leeg lichaam met
"Body cannot be empty when content-type is set to 'application/json'" -
vóórdat er ook maar één hook of route-handler heeft kunnen draaien, dus nog
vóór req.user gezet wordt en vóór enige try/catch in een route. Dat verklaart
waarom eerdere per-route foutafhandeling dit nooit kon vangen: de fout zat
vóór elke route.
- public/js/core.js: api() stuurt Content-Type alleen nog mee als er
werkelijk een JSON-body is
- src/body-parser.js (nieuw): serverside vangnet dat een leeg lichaam bij
Content-Type: application/json gewoon toestaat i.p.v. te weigeren -
ongeacht wat een client meestuurt
- test/body-parser.test.js: reproduceert de exacte foutmelding tegen een
echte Fastify-requestcyclus (de gemockte-pool-tests konden dit principieel
nooit vangen, vandaar dat dit alle eerdere testrondes overleefde)
- de generieke errorhandler gaf alleen detail mee als req.user al gezet was;
bleek de fout zelf al vóór/tijdens dat moment te zitten (zoals bij klas
verwijderen), dan bleef het toch de kale "serverfout" - nu altijd de echte
oorzaak, dit is een intern beheertool zonder publieke bezoekers
- klasnamen konden onbeperkt dubbel aangemaakt worden binnen dezelfde school
(geen enkele controle) - POST /admin/classes weigert dit nu met 409, en een
nieuwe unieke index (school_id, lower(name)) is het echte vangnet
- migratie 016 hernoemt eerst bestaande dubbele klasnamen (niets verwijderd)
vóórdat de unieke index wordt gezet: anders was de migratie zelf op deze
database mislukt en had de app niet meer opgestart
- DELETE /admin/classes/:id had geen try/catch: elke mislukking (bv. een FK
die nog niet cascade/set-null is op deze database) viel terug op de
generieke handler en toonde alleen "serverfout" zonder aanknopingspunt
- zelfde aanpak als eerder bij het verwijderen van een gebruiker: de echte
Postgres-foutmelding komt nu terug i.p.v. een kale "serverfout"
- test toegevoegd voor zowel het gewone pad als de foutmelding
- linkedParents() gebruikte "SELECT DISTINCT p.* ... ORDER BY lower(p.username)":
Postgres weigert dit omdat de ORDER BY-expressie niet letterlijk in de
select-lijst staat. Deze query loopt bij élke school-gescopede
gebruikerslijst (elke schoolbeheerder, en elke systeemmanager die een
specifieke school kiest i.p.v. "alle scholen") - dus die lijst laadde
nooit, klassen werden om dezelfde reden niet (opnieuw) opgehaald, en
"alle scholen" was de enige werkende weergave
- fix: lower(p.username) expliciet als kolom (sort_key) in de select-lijst,
ORDER BY daarop i.p.v. op de losse expressie
- regressietest die de exacte SQL-vorm vastlegt (een gemockte pool voert geen
echte SQL uit en had deze Postgres-grammaticafout niet kunnen vangen)
- DELETE /admin/users/🆔 hele handler (niet alleen deleteUserFully) in
één try/catch, zodat ook een fout vóór het verwijderen zelf (bv. de
opzoekquery) de echte reden teruggeeft i.p.v. terug te vallen op de
generieke 500 "serverfout"
- generieke errorhandler logt voortaan altijd het volledige fouteobject en
geeft ingelogde gebruikers de werkelijke foutmelding mee i.p.v. alleen
"serverfout"
- test toegevoegd die dit nieuwe pad dekt
- De "serverfout" bij verwijderen komt van een foreign key die op de live-database geen ON DELETE CASCADE heeft, waardoor DELETE FROM users faalt zodra er nog een gekoppelde rij bestaat
- Nieuw: de verwijderroute haalt in één transactie eerst alle afhankelijke rijen weg (sessies, rollen, klaskoppelingen, toewijzingen, voortgang, ouderkoppelingen, aanmeldingen, reservekopieën, gedeelde items, afbeeldingen, borden/mappen) en verwijdert daarna de gebruiker - dit werkt ongeacht de exacte FK-instelling en heeft migratie 015 niet nodig
- Mocht verwijderen tóch falen, dan komt nu de echte databasereden terug (constraint-detail) i.p.v. een kale "serverfout", zodat de oorzaak zichtbaar is
- 3 servertests: volledige opruiming in transactievolgorde (afhankelijke rijen vóór users), echte foutreden bij een gesimuleerde fout; 97 tests groen
LET OP: dit werkt pas na deploy van deze versie; v0.3.94 (huidige live) bevat de fix nog niet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Verwijderen gaf op de live-database een 500 "serverfout". Oorzaak: op databases die zijn ontstaan vóór alle foreign keys ON DELETE CASCADE waren, houdt een tabel de oude constraint (CREATE TABLE IF NOT EXISTS werkt een bestaande constraint nooit bij), waardoor DELETE FROM users een foreign-key-fout geeft
- Migratie 015: zoekt elke FK naar users(id) die niet cascade en niet set-null is en zet die alsnog op ON DELETE CASCADE (bewuste SET NULL-constraints blijven ongemoeid). Draait bij de volgende deploy en is idempotent
- Verwijderroute vangt een resterende FK-fout (23503) op als een begrijpelijke 409 i.p.v. een kale serverfout
- Geverifieerd: alle INSERT INTO users-paden vangen al 23505 op (gebruikersnaam uniek per school); dezelfde naam in verschillende scholen is bewust toegestaan (inloggen kiest de school) en staat sinds 0.3.94 per school gegroepeerd
- 3 nieuwe servertests (FK-fout -> 409, normale verwijdering, migratie-inhoud); 97 tests groen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Onzichtbare knoppen opgelost: .tbtn houdt standaard witte tekst (wit-op-blauw), maar in het beheer staat hij op een lichte achtergrond -> tekst was wit-op-wit. Nu expliciet donkere tekst; de primaire accentknop houdt bewust witte tekst
- Systeemmanager kreeg standaard "alle scholen" waardoor klassen leeg leken en niet toe te voegen waren; er wordt nu meteen een concrete school gekozen (klassen/gebruikers direct gevuld). Contextbalk laat wisselen; "alle scholen" blijft een bewuste keuze
- Lege school toont nu een duidelijke uitleg dat bestaande klassen bij een andere school kunnen horen, met verwijzing naar de schoolkeuze
- Correcte gegevensregels: classes.school_id is NOT NULL met ON DELETE CASCADE, dus een klas kan niet "los" bestaan (die eerdere aanname is teruggedraaid). Een klas is nu te VERPLAATSEN naar een andere school (PATCH weigert null/ongeldig); een verwijderde school neemt zijn klassen mee (cascade) - dat verklaart verdwenen klassen
- Bij een leerling naar een andere school koppelen blijft de klas behouden als die al bij de doelschool hoort
- Verificatie uitgebreid met een echte kleurcontrast-scan (vangt wit-op-wit) over alle secties + de default-school-flow; alle 94 servertests en 8 Playwright-suites groen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Leerlingen stonden onder standaard-ingeklapte klasgroepen, dus met een gekozen school leek de lijst leeg terwijl de detailweergave de juiste school toonde. Klasgroepen staan nu standaard open zodra een school is gekozen (de schoolkeuze wás al het filter); bewust dichtklappen wordt onthouden
- Losse klassen (school verwijderd) waren nergens zichtbaar: de systeemmanager ziet ze nu zonder gekozen school met een uitleg, en kan ze via een School-veld terugkoppelen; nieuwe PATCH /admin/classes/:id + GET ?school=none (super) met servertests
- Bij het aan een school koppelen van een leerling blijft de klasindeling behouden als die klas al bij de doelschool hoort (eerst klas terugkoppelen, dan de leerlingen)
- UI-consistentie: één uniforme control-hoogte (32px) voor álle knoppen, selects en invoervelden in Instellingen - einde aan de grote maatverschillen; T() valt terug op NL en dan de sleutelnaam zodat een knop nooit zonder tekst kan staan
- Status in de gebruikerstabel als compacte glyph (● in gebruik / ○ wacht) met uitleg bij aanwijzen i.p.v. een afgekapte "nog niet ing…"-chip
- Geverifieerd in headless Chromium: nieuwe suite (21 checks: zichtbaarheid met schoolfilter, losse klassen, geen lege knoppen, uniforme hoogte over 8 secties) + volledige regressie van 5 bestaande suites
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- PATCH /admin/users/:id ondersteunt schoolId (alleen systeemmanager, rollen teacher/admin/pupil): null = losmaken, anders koppelen met bestaanscheck (404); bij wissel worden klas, klaskoppelingen en (voor leerlingen) oude toewijzingen opgeruimd; naamconflicten in doelschool of school-loze naamruimte geven een nette 409
- Detailpaneel toont voor de systeemmanager een School-veld bij deze rollen - losse accounts (ooit aan een school gekoppeld) zijn hiermee terug te zetten en bewust los te maken; werkt ook in de beheerder-subrijen op het Scholen-tabblad
- Gebruikers-tab (alle scholen): losse accounts staan in een eigen inklapbare groep "Zonder school (n)" i.p.v. onopvallend tussen de rolgroepen; de oudergroep is nu ook inklapbaar
- 3 nieuwe servertests (koppelen incl. opruiming, losmaken, 404, geen rechten/no-op voor admin en parent, 409 bij naamconflict) + 10 Playwright-checks voor de volledige flow
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Migratie 014: site_content (sleutel/waarde, JSONB) voor site-inhoud die zonder release aanpasbaar moet zijn
- GET /content (publiek): welkomstwoord, update-melding en widget-uitleg in één antwoord - het inlogscherm toont het welkomstwoord al vóór het inloggen
- PUT /admin/content/:key (alleen systeemmanager, nieuwe permissie content.manage): upsert per toegestane sleutel (welcome/announcement/widgetInfo) met maat-limieten
- 4 servertests: publiek lezen, upsert door super, 403/401 voor anderen, whitelist/vorm/omvang-validatie
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- @fastify/compress (brotli quality 4 + gzip, global): alle statische assets én API-JSON (zoals /me/data) gaan gecomprimeerd over de lijn - eerste lading van ~715 KB naar grofweg een derde
- Nieuw src/frontend.js: index.html wordt bij boot één keer gelezen, ?v=__V__ wordt vervangen door de app-versie en de pagina wordt uit het geheugen geserveerd met no-cache
- Cache-beleid per prefix: js/css een jaar immutable (veilig dankzij de geversioneerde URL's), fonts 7 dagen, afbeeldingen 1 dag met zwakke etag als 304-vangnet - herhaalbezoek kost vrijwel geen bytes meer
- Alle 31 scripts staan nu met defer in de head: parallel downloaden, uitvoeren in dezelfde dwingende volgorde (core → widgets → board → app); font-preload bewust ongeversioneerd (moet byte-gelijk blijven aan de url() in teach.css)
- Compress geregistreerd vóór de api/frontend-registraties (onSend-hook geldt alleen voor latere routes) - geborgd met een teksttest
- 6 nieuwe tests (test/frontend.test.js): versie-injectie, cache-headers per prefix, gzip+brotli round-trip, 304 op etag, defer/?v= op elk script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Nieuwe architectuur: één sectieregister (settings.js) als enige bron van waarheid; secties melden zich aan met rol-zichtbaarheid, activate() en zoekgegevens - de schil kent het beheer niet van binnen
- Nieuwe indeling: navigatie links (gegroepeerd Persoonlijk / School / Systeem), inhoud rechts met sectiekop + toelichting; zakelijk en rustig, responsief op smalle schermen
- Alles doorzoekbaar: één zoekbalk vindt secties, instellingen (trefwoorden, o.a. wachtwoord/thema/taal) én data - gebruikers, klassen en scholen op naam; klik navigeert en filtert de gebruikerslijst direct op de treffer
- Per rol alleen de essentie: gast alleen Weergave; leerling en ouder Account + Weergave; groepsleiding + Gebruikers/Toewijzingen/Voortgang; schoolbeheerder ook Klassen; systeemmanager ook Systeem en Opslag
- Thuiskinderen (leerling zonder school) krijgen nu zoals ontworpen het volledige whiteboard met eigen opslag en reservekopieën; schoolleerlingen blijven beperkt tot de toewijzing
- Oude dubbele tabbalk en #adminTabsSlot verwijderd; beheerdata laadt één keer per bezoek en wordt tussen secties hergebruikt
- 28 Playwright-checks over zes rollen (navigatie, zoeken, filteren, thuiskind-whiteboard)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Ouders (school_id NULL) vielen buiten elke schoolgescopeerde gebruikerslijst; /admin/users levert nu ook de ouders die aan leerlingen van de school gekoppeld zijn (groepsleiding: alleen ouders van leerlingen uit eigen klassen)
- Ouder-rijen tonen de namen van hun gekoppelde kinderen (childNames)
- Servertest voor de gekoppelde-ouders-lijst
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Nieuwe rol parent (school_id NULL): registreert zichzelf via POST /auth/register-parent (rate-limited, stafwachtwoordregels, directe sessie)
- Migratie 013: parent_children-koppeltabel, eenmalige users.parent_code, school_link_requests (aanmeldverzoeken, uniek per kind+school)
- Ouderroutes: kinderen bekijken (met school/klas/aanmeldstatus), thuiskind aanmaken (pupil zonder school, wachtwoord eenmalig terug), koppelen via ouder-code, aanmelden bij school, ontkoppelen
- Staf: ouder-code genereren per leerling, aanmeldingen bekijken en afhandelen (adopteren in klas met optionele nieuwe gebruikersnaam, koppelen aan bestaande leerling met opruiming van lege thuisaccounts, afwijzen)
- Voortgang: thuiskind zonder toewijzing logt met de gekoppelde ouder als leerkracht (bord "thuis"); gekoppelde ouders mogen de voortgang van hun kind opvragen
- 18 nieuwe servertests (registratie, koppelmodel, aanmeldflow, resolve-acties, voortgang)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- Functie-select in gebruikersbeheer alleen zichtbaar voor staf-rollen (teacher/admin/super)
- Klaskoppeling-UI alleen voor staf-rollen
- Server: rolwijziging alleen tussen staf-rollen onderling (leerlingen en toekomstige ouder-accounts kunnen nooit promoveren)
- Server: klaskoppeling weigert alle niet-staf-rollen expliciet
- canPersist() sluit de komende ouderrol uit van borden-opslag
- Servertest: ouder-achtige rol krijgt geen promotie en geen klaskoppeling
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
- De klas-koppeling (chips + "voeg toe aan klas…") staat nu bij elke
staf-rol, niet alleen bij groepsleiding; de klasnamen staan ook in de
rijkop ("Schoolbeheerder · Groep 4")
- Server: /admin/classes/:id/teachers accepteert alle staf-rollen; alleen
leerlingen niet (die horen al bij één klas). Een systeemmanager is
school-onafhankelijk en mag aan klassen van elke school; overige staf
moet bij dezelfde school horen
- Nieuwe servertest: admin en super koppelen ok, leerling en staf van een
andere school geweigerd
- Het extra-functies-systeem (chips + select door elkaar) is uit de UI:
een gebruiker heeft één functie, alleen te wijzigen via de Functie-select
- Groepsleiding is nu eenvoudig aan klassen te koppelen: huidige klassen
als chips met ✕, een select "voeg toe aan klas…" voor nieuwe koppelingen;
de klassen staan ook in de rijkop ("Groepsleiding · Groep 4")
- /admin/users geeft de klas-koppelingen (class_teachers) per gebruiker mee
- Formulier-fouten (bv. ongeldige gebruikersnaam bij een nieuwe
systeemmanager) verschijnen nu direct bij het formulier in plaats van
onzichtbaar bovenaan - dit maakte aanmaken schijnbaar onmogelijk
- Wachtwoord/koppelcode van een nieuwe gebruiker verschijnt direct bij die
gebruiker in de lijst (rij en groep klappen vanzelf open)
- Systeem toont per school een rij in klassen-stijl (🏫, accentkleur,
aantal schoolbeheerders) met daaronder de schoolbeheerders als duidelijk
onderscheiden subrijen (🛡, ingesprongen, eigen randkleur)
- Beide zijn in dit menu aan te passen via hetzelfde ✏-gereedschap:
de school heeft een Naam-veld (nieuwe route PATCH /admin/schools/:id,
alleen systeemmanager) en de bevestigde verwijder-actie; beheerders
hebben hun volledige gereedschap (functie, extra functies, code,
wachtwoord, verwijderen)
- Systeem-tab laadt beheerders van álle scholen, ook als de contextbalk
elders op één school staat
- Drie nieuwe servertests voor het hernoemen (super ok, admin 403,
naamvalidatie)
- Nieuwe migratie db/011: shared_library.readonly (default false)
- Beheerregel aangescherpt: een vergrendeld school-item is ook voor de
eigenaar alleen-lezen; alleen schoolbeheerder en systeemmanager kunnen
het nog wijzigen (systeem-items waren al alleen-lezen voor iedereen
behalve de systeemmanager) - kopiëren blijft altijd werken
- De vlag zetten/weghalen kan per item (PATCH readonly) of per map
(PATCH /shared/:kind/folder met {from, readonly}), uitsluitend door de
schoolbeheerder van die school of de systeemmanager
- Lijst/detail geven readonly + canLock terug voor de UI; drie nieuwe tests
- Nieuwe migratie db/010: shared_library-tabel (school_id NULL =
systeembreed, anders per school; kind board/anchor; folder als pad)
- Routes: GET /shared/:kind (lijst, gefilterd op systeembreed + eigen
school), GET /shared/:kind/:id (met data, voor kopiëren), POST (publiceren;
scope global vereist systeemmanager, scope school staf met school),
PATCH/DELETE per item en PATCH /shared/:kind/folder (prefix-rewrite,
alleen als álle items onder het pad beheerd mogen worden)
- Beheerregel: systeemmanager altijd, eigenaar altijd, schoolbeheerder voor
items van de eigen school; leerlingen hebben geen toegang
- Nieuwe permissies shared.global.manage / shared.school.manage (beide
permissions.js); zes nieuwe servertests
- roleSuper heet nu "Systeemmanager" (en: "System manager"); het
beheertabblad en de stafgroep "Overkoepelend" heten nu "Systeem"
- Alle overige teksten en commentaren met "overkoepelend beheerder"
meegenomen; de interne rolcode "super" blijft ongewijzigd
- Nieuwe ankerbibliotheek ANCHOR_LIB, opgeslagen in een eigen data-sectie
"anchors" (server-whitelist + 5MB-cap; lijst i.p.v. object toegestaan);
reist mee in de volledige save, hydrate, gastreset en reservekopie-herstel
- Ankerwidget: 💾 bewaart het huidige diagram (naam = middelwoord, met
✓-feedback; leeg diagram wordt geweigerd met een hint), 🗂 opent dezelfde
Windows-stijl mappenverkenner met de bewaarde diagrammen (geneste mappen,
slepen, hernoemen, verwijderen)
- Dubbelklik op een bewaard diagram laadt het in het widget (cellen +
plaatjes, thema-spiegel en bord-autosave volgen automatisch)
- Nieuwe servertest: anchors-sectie accepteert een lijst, andere secties
weigeren die (en andersom)
- Nieuwe route PUT /me/data/:section (boards/themes/generalExtra): vervangt
alleen die sub-key via jsonb_set, met dezelfde rev/409-controle en
snapshot-hook als de volledige PUT (die blijft bestaan als terugval)
- Per-sectie size-caps (boards 20MB, themes/generalExtra 2MB)
- core.js: DIRTY-set houdt bij wat er gewijzigd is; woordwijzigingen sturen
alleen themes+generalExtra, bordwijzigingen alleen boards (sequentieel,
rev per respons bijgewerkt); zonder gerichte wijziging volledige save
- Twee nieuwe servertests (jsonb_set op de juiste sub-key, onbekende sectie
is 404)
- Elke data-write bewaart eerst de oude data in user_data_versions (hooguit
één snapshot per 10 minuten, maximaal 10 versies per gebruiker)
- Nieuwe routes: GET /me/data/versions (lijst) en POST /me/data/restore
(zet een versie terug; bewaart de huidige data eerst zodat herstel zelf
ook omkeerbaar is); beide niet voor leerlingen
- Nieuw blok "Reservekopieën" in Instellingen → Account met datum/tijd per
versie en een herstelknop met bevestigingsstap (arm-then-confirm)
- Vier nieuwe servertests (snapshot vóór write, opruimen op 10, pupil-403,
restore bewaart huidige data eerst)
- Nieuwe migratie db/009: users.data_rev + data_updated_at en de
user_data_versions-tabel (voor de komende versiegeschiedenis)
- GET /me/data geeft nu {data, rev}; PUT /me/data controleert een optionele
x-data-rev-header en weigert een verouderde schrijver met 409 (zonder
header blijft het oude laatste-schrijver-wint-gedrag voor oude clients)
- core.js: rev wordt bijgehouden en meegestuurd; bij 409 toont de opslaan-
knop "Elders bewerkt" en haalt een klik eerst de nieuwste serverversie op
in plaats van te overschrijven
- beforeunload-waarschuwing bij een openstaande, lopende of mislukte save
- Nieuwe test test/save-system.test.js (rev mee, 409, legacy zonder header)
- Nieuwe migratie db/008_widget_share.sql: assignments krijgt widget_id
(NULL = heel bord, bestaand gedrag) en mode ('kijken'/'werken',
default 'werken')
- PUT /assignments/class/:id en /assignments/pupil/:id accepteren optioneel
widgetId + mode; gedeelde validatie (parseShareBody) controleert dat de
widget echt op het opgegeven eigen bord staat
- describeAssignment geeft widgetId/widgetType/mode terug voor de UI
- GET /my/assignment filtert bij een widget-toewijzing tot die ene widget en
stuurt de modus mee
- board.js: genWidgetId() naar het voorbeeld van genBoardId(); toegekend in
makeWidget (bestaand wid uit opgeslagen data wint), bewaard in
serializeBoard; dupliceren geeft een nieuw wid; oude borden zonder wid
krijgen er vanzelf een bij de eerstvolgende save
- /my/assignment stuurt wid mee per widget
- pupil.js: voortgang-widgetId gebruikt nu het wid (terugval op positie voor
borden van vóór deze versie)
Voorbereiding op het delen van één losse widget (roadmap item 8).
- Nieuwe migratie db/007_progress.sql: progress_events-tabel (per leerling
per widget-moment: pogingen, goed, sterren)
- POST /my/progress: leerling logt een afgerond moment; teacher_id/board_id
worden server-side afgeleid uit de bestaande toewijzing, nooit van de client
- GET /progress/pupil/:id en GET /progress/class/🆔 leerkracht-weergave,
gated met nieuwe progress.view-permissie en dezelfde scoping als assignments
- Permissie progress.view toegevoegd in beide permissions.js-bestanden
- Nieuwe tabel `assignments`: een leerkracht koppelt één van haar eigen
borden aan een klas (standaard) of een individuele leerling (uitzondering,
voor wie op een ander niveau werkt).
- Geen momentopname: /my/assignment zoekt het bord live op bij de
leerkracht, dus nieuwe woorden die zij toevoegt komen vanzelf door zodra
de leerling-pagina opnieuw pollt (elke 20s).
- Leerlingen loggen in op een vereenvoudigde pagina zonder whiteboard/
tekengereedschap: enkel de toegewezen taal-/rekenwidgets, als vaste
(niet-sleepbare) kaarten in speelmodus.
- Speelmodus verbergt bewerkfuncties in Ankerwoorden (plaatje kiezen,
woord typen, kleur wijzigen) en Letterblokken (thema/woordbeheer) -
overige widgets waren al zuiver speelgericht.
- Elk bord krijgt een stabiel id (board.js) zodat een toewijzing een bord
blijft vinden ook na hernoemen of herordenen.
- Nieuw tabblad "Toewijzingen" in het beheerpaneel: per klas of leerling
een eigen bord kiezen, met een expliciete toewijs-stap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuxHJtk5aHe2pA3xwCLEAk