Compare commits

..

225 commits
main ... dev

Author SHA1 Message Date
Ramon
fbe1f8c876 feat: nieuwe proefplaat "Egeltje in het bos" (groep 1-2, v0.4.84-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 46s
- Nieuwe kleurplaat op verzoek: rondere, zachtere dierenstijl geïnspireerd
  op publiek-domein referentiebeelden (openclipart.org, bekeken maar niet
  overgenomen), niet de exacte scenes uit de aangeleverde voorbeelden
- Egel getekend met een scherpe, doorlopende zigzag-stekelrand als
  onderdeel van de romp zelf, in plaats van losse ronde "bladvorm"-plukjes
  erbovenop - dat laatste las als bloemblaadjes, niet als stekels
- Vier poten met een ronde poot-vorm en teentjes in plaats van rechte
  blokjes; buik is één rustige boog zodat de poten er voorspelbaar
  onderuit steken zonder door elkaar of de buiklijn heen te kruisen
- Boom herbouwd: stam die breed begint bij de grond en versmalt naar
  boven (net als de getapte dierenpoten van eerder deze sessie, maar
  omgekeerd), met een zichtbare splitsing in takken en een kroon van drie
  overlappende ronde plukken in plaats van één gladde ovaal op een paal
- Testinvariant "20 platen, 5 per groepenband" versoepeld naar "minstens
  20/5" zodat een proefplaat als bonus kan bestaan zonder de gecureerde
  kernset aan te tasten

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-31 19:52:49 +02:00
Ramon
f3c30b5d8b fix: hals van giraffe en zebra echt laten toelopen, sauropode poten (v0.4.83-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- Halzen van giraffe en zebra tekenden als een rechte koker met vrijwel
  gelijke breedte overal ("paal met een hoofd erop"); ze lopen nu duidelijk
  toe van brede schouder naar smalle kop, met een vloeiende overgang
- Snuit van de zebra zat als losse donkere wig tegen de kop geplakt ("muis
  met een puntmuts"); kop en snuit zijn nu één doorlopende vorm
- Boomstam-textuurlijnen bij de savanneplaat staken onder de nieuwe
  zebrakop uit; ingekort tot ruim boven het dier
- Sauropode-poten waren nog rechte rechthoeken; nu getapte poten in dezelfde
  stijl als giraffe/zebra, met genoeg tussenruimte om niet aan het oog
  onttrokken te raken door de poot ervoor
- Oogpitje van de sauropode stond net als eerder bij kikker/libelle/
  giraffe/zebra in de niveau-2-detailgroep, dus geen gezicht op niveau 1;
  nu altijd zichtbaar
- Kikker en konijn beoordeeld en in orde bevonden: kop-op-romp zonder
  zichtbare hals is bij die dieren een geldige, al werkende opbouw

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-31 00:00:30 +02:00
Ramon
67553361b8 fix: kikker en savannedieren grondig herzien (v0.4.82-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- Kikker in de vijverplaat herbouwd: brede achterpoten opzij en kleine
  voorvoeten vooraan in plaats van twee identieke dunne armen die als
  een teddybeer lazen; grassprietjes verplaatst die over de nieuwe voet liepen
- Boomstam bij de savanneplaat smalt nu naar rechts toe in plaats van te
  verbreden, en de zebra is 45px opgeschoven: kop en oor liepen eerder
  dwars door de stam heen
- Poel onder de dieren meeverschoven met de zebra
- Systemische fout gevonden en gefixt in de vijver- en savanneplaat: de
  oogpitjes van kikker, libelle, giraffe en zebra zaten in de tier-2
  detailgroep, dus op niveau 1 (de eenvoudigste stand) hadden deze dieren
  helemaal geen ogen. Nu staan de pitjes op niveau 1 al zichtbaar, terwijl
  het oogvak zelf pas op niveau 2 verschijnt als invulwerk

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 22:39:29 +02:00
Ramon
84fd02d135 fix: libelle en dierenvormen herstellen, instellingen omdraaien (v0.4.81-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- Libelle in de vijverplaat kreeg weer vier symmetrische vleugels
- Zeepaardje in het koraalrif herbouwd van een onherkenbare buis naar een
  duidelijk zeepaardje, verplaatst zodat het niet meer over de clownvis valt
- Nieuwe coloringLegPath()-vorm: giraffe en zebra hebben nu getapte,
  licht geknikte poten in plaats van rechte kokerpoten
- Instellingenindeling omgedraaid: de kleurplaat (thema/plaat) is weer
  direct zichtbaar, kleurhulp/reset gaan achter het tandwiel
- Letterblokken en rekenwidgets: niveauknoppen naar het tandwiel, het
  woord/thema blijft direct zichtbaar
- Klok, geldrekenen, plaatswaardetabel, boom&blad/vogels/planten en
  topografie: spel/activiteit (en bij geld/plaatswaarde het bijpassende
  veld) staan weer in de vaste bovenbalk, niveau staat achter het tandwiel
- Plaatswaardetabel toont nu ook in verkennen-stand een vaste bovenbalk
  (spel, getal, dobbelsteen) in plaats van alleen in bouwen-stand
- Bugfix onderweg: niveauwissel in verkennen-stand van de plaatswaardetabel
  sprong per ongeluk naar bouwen-stand - nu blijft de stand behouden

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 21:39:30 +02:00
Ramon
d8610b010d feat: compacter instellingenmenu voor acht widgets (v0.4.80-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
Bij veel widgets nam de instellingenstrook te veel hoogte in, vooral bij een
smal widgetvenster waar de rij naar meerdere regels uitwaaierde. Nieuw
gedeeld patroon: besturing die voortdurend nodig is (kleur, niveau,
woordbron) blijft in één compacte rij zichtbaar; besturing die maar zelden
wordt aangepast (thema, geluid aan/uit, invoerwijze, bereik) gaat achter een
inklapbaar ⚙-paneel. Dit patroon bestond al voor klok/geld/rekengetallen/
wereld (learningShell() in learning.js) - nu als widgetGear()/gearField() in
core.js beschikbaar voor de rest van de app, met dezelfde .edu-settings-
opmaak zodat het overal hetzelfde oogt en werkt.

- kleurplaten: twee volle rijen (thema/plaat + kwast/emmer/gum/kleur/dikte/
  hulp/reset/klaar) teruggebracht naar één vaste rij (gereedschap, kleur,
  dikte, klaar) plus het ingeklapte paneel (thema, plaat, kleurhulp, reset).
  Losstaand: coloring.css gebruikte @media(max-width:620px) in plaats van de
  @container widget-conventie die de rest van de app gebruikt, waardoor die
  regels nooit afgingen bij het verkleinen van het widgetvenster zelf - nu
  gecorrigeerd.
- letterblokken: geluid, invoerwijze en aantal kaarten in het paneel; niveau
  en woordbron blijven vast.
- rekenwidgets (optellen/aftrekken/keer/delen/procenten): geluid en
  invoerwijze in het paneel; niveau blijft vast.
- ballonnenwoord: geluid in het paneel.
- flitswoorden: flitstijd in het paneel.
- getallenlijn: het hele bereik-instelblok (van/tot/stap/nieuw) in het
  paneel - dat stond toch al uit voor leerlingen met een beheerd niveau.
- zinsbouwer: het rijtje voor eigen woorden toevoegen in het paneel; de
  speelknoppen (verrassing/ongedaan/wissen/voorlezen) blijven vast.
- timer: geluid en weergavevorm in het paneel; starttijden en start/reset
  blijven vast.
- geluidsmeter: de twee drempelschuifjes plus de privacynotitie in het
  paneel.

Widgets die dit patroon al hadden (klok, geld, breuken, plaatswaarde,
woordsoorten, bomen/vogels/planten, topografie, stemming, stemming-poll,
dagplanning) of waar de rij al compact genoeg was (schrijfblad, hakken en
plakken, dobbelstenen, notities, naamtrekker) zijn ongewijzigd gelaten.

Geverifieerd door elke aangepaste widget live via de echte makeWidget()/
REGISTRY in een headless browser te openen, het paneel open en dicht te
klappen en te controleren op consolefouten - ook op de minimale
widgetbreedte (coloring, minW 390). 202 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 16:46:37 +02:00
Ramon
f4a4cbd28c feat: herteken vierseizoenenboom en dino, herstel windmolen (v0.4.79-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m4s
Drie platen die na eerdere reparaties nog niet klopten, nu fundamenteel
anders aangepakt in plaats van bijgeschaafd.

Boom van vier seizoenen: het idee van één cirkel in vier kwarten werkte niet,
ook niet met ronde plukken op de rand - het bleef een bal met een kruis erin.
Vervangen door vier losse boompjes naast elkaar, één per seizoen, elk met een
eigen kroonvorm: lente (bloesem met groene blaadjes), zomer (vol groen blad
met wat vruchten), herfst (oranje/geel blad, twee vallende blaadjes op de
grond) en winter (kale boom - drie echte, gevulde takken vanuit verschillende
hoogtes op de stam, met dunne twijgjes en sneeuwklompjes op de uiteinden).
Eerste versie van de kale winterboom liet takken vanuit één punt uitwaaieren
en oogde als vuurwerk; met gevulde taksegmenten en een aftakpunt per tak is
het nu een boom.

Dinosaurus: nog niet herkenbaar genoeg. Volledig hertekend naar de klassieke,
direct herkenbare langnekdino-verhoudingen: een veel langere gracieuze hals,
een groot rond lijf, een lange dunne staart en een klein koppie met een grote
vriendelijke oog - in plaats van de kortere, gedrongen versie van de vorige
ronde.

Windmolen in de duurzame stad: de drie wieken stonden niet gelijk verdeeld
(twee dicht bij elkaar, één ver weg) en één wiek liep dwars door de zon heen.
Nu netjes 120 graden uit elkaar en gedraaid zodat geen enkele wiek de zon
raakt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 15:25:22 +02:00
Ramon
2731fb3469 fix: herstel losse bladeren, een dinosaurus en dierenpoten (v0.4.78-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 37s
Gecontroleerd door elke plaat op ware grootte te renderen en niet op de
gemiddelde vinkjes te vertrouwen; drie echte problemen bleken over het hoofd
gezien te zijn.

Losse bladeren die kaal aan de boom hingen:
- boom van vier seizoenen: de kroon was een gladde cirkel met losse spitse
  blaadjes op de rand, die als ogen of aangeplakte snippers lazen. Vervangen
  door ronde plukken ruim binnen elk kwart, in drie maten zodat ze niet als
  een paar ogen naast elkaar staan.
- regenwoud: de bladeren zweefden los in de lucht tussen de twee stammen,
  met alleen een puntig uiteinde dat de stam raakte. Beide stammen kregen nu
  een echte kroon erbovenop, met bladplukken erin.

De dinosaurus was echt vreemd: de hals was een rechte dikke staaf met een los
rondje erboven als kop, en de poten waren twee brede lappen die als "twee
poten" oogden. Volledig hertekend: een S-vormige hals, kop en snuit als één
doorlopende ovale vorm met oog, neusgat en mond, en vier duidelijk
gescheiden poten met elk een eigen voet.

Giraf en zebra op de savanne hadden wel vier potervakken, maar alleen de
achterpoten hadden een hoef - de voorpoten liepen dood zonder afsluiting en
vielen daardoor optisch weg, waardoor het dier op twee poten leek te staan.
Alle vier de poten hebben nu een eigen hoef.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 15:02:12 +02:00
Ramon
308f877be6 feat: herteken de resterende zwakke kleurplaten (v0.4.77-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
De twee platen die ik zelf had aangemerkt plus de overige zwakke plekken uit de
visuele controle. Audit blijft schoon op alle drie de niveaus: geen
onbereikbare vakken, geen onzichtbare of afdwalende lijnen.

Dieren met vier poten in plaats van twee latjes:
- savanne: giraf en zebra hebben nu vier poten met een knik en een hoef, een
  langere en slankere giraffennek die breed uitloopt in de schouder, een kop
  met snuit, ossenhoorns en oor, en een manestreep bij de zebra.
- dinosaurusvallei: vier poten met voeten en een staart die echt versmalt.

Vormen die verkeerd lazen:
- boom van vier seizoenen: de kroon was een gladde cirkel met een kruis erin en
  las als een bal. De vier seizoenskwarten hebben nu een geschulpte buitenrand,
  dus het leest als bladerdek. De losse plukken op de rand zijn vervangen door
  bladclusters binnen elk kwart, waardoor ook de inkepingen verdwenen die
  ontstonden waar plukken en bladvormen elkaar overlapten.
- koraalrif: het zeepaardje was een smal buisje dat als worm las. Nu een
  stevige romp met een aparte gekrulde staart, de rugvin op de rug in plaats
  van dwars door het lichaam, en meer buikringen.
- vrienden in zee: de schildpad stond klem tegen de rechterrand; hij is groter
  en verder naar binnen geschoven, met grotere schildplaten.

Lege vlakken gevuld, want er was niets te kleuren:
- kleine tuindieren: zon, wolk en een vlinder in de bovenhelft.
- koraalrif: een schooltje visjes in de lege bovenhoek; de bellen zijn
  meegeschoven.
- feesttaart: een tafelkleed met schulprand in de onderste strook.
- grote graafmachine: zonnestralen, zoals bij de andere zonnen.

Losse elementen vastgezet:
- regenwoud: de bladeren aan de rechterkant zweefden los naast de stam en
  sluiten nu aan, met bladnerven op niveau 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 14:40:30 +02:00
Ramon
e800467745 fix: verbeter het lijnwerk van de kleurplaten (v0.4.76-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
Systematisch nagelopen met een meetkundige audit in de browser plus een
visuele controle van alle 20 platen op ware grootte (800x600). De audit meet
per niveau welke kleurvakken volledig achter latere vakken verdwijnen, welke
detaillijnen onzichtbaar zijn en welke lijnen buiten het blad vallen.

Onbereikbare kleurvakken verholpen (waren onzichtbaar én niet te kleuren):
- kleine tuindieren: de kop van het lieveheersbeestje zat volledig achter het
  schild; de kop komt nu boven het lichaam uit.
- molen in de polder: de onderste wiek verdween achter de romp. De wieken
  staan nu als schuin kruis, zodat alle vier vrij liggen, met latwerk erin.
- regenwoud: een mosplukje zat achter de ara.
- kasteel: de verre heuvels waren voor 89% bedekt en zijn opgeschoven.
- leven rond de vijver: twee bloemblaadjes van de waterlelie zaten achter de
  vis; de bloem staat nu naast de vis.

Overtollige strepen weggehaald:
- graspollen in vlinder, tuindieren, molen en vogelnest stonden los in het
  gras; ze beginnen nu op de grondlijn zelf.
- feesttaart: losse kruisjes in de lucht lazen als vergissingen, nu confetti-
  stippen; de ballontouwtjes lopen door tot op de tafel.
- bergkamp: de graatlijnen liepen door tot in het meer en stoppen nu bij de
  bergvoet.
- boom van vier seizoenen: een losse kromme stond midden in de kroon.
- kasteel: het bootje overlapte de brug, waardoor zeil en brugdek door elkaar
  heen liepen; het ligt nu links in de rivier.
- koraalrif: de rugvin van het zeepaardje stak dwars door het lichaam.

Ontbrekende strepen toegevoegd:
- schorslijnen op de stammen van picknick en regenwoud.
- glazuur op de middelste laag van de taart (alleen boven- en onderlaag hadden
  het).
- sneeuwkappen op beide bergtoppen zijn groter en echt zichtbaar.
- fanlijnen in het fankoraal, bloemblaadjeslijnen in de bloesems.

Vormen die verkeerd lazen hertekend:
- tulpen bij de molen hadden een ronde koepel en lazen als paddenstoelen; nu
  een puntige kelk van drie blaadjes.
- lensvormen die als ogen of blaadjes lazen zijn ronde plukken geworden in de
  herfstboom, de acacia (met een derde pluk om het "ogenpaar" te breken) en de
  kroon van de vierseizoenenboom.
- de kikker had lensvormige poten naast zich; nu poten met knik en voet.
- graspollen op de savanne zijn tuiltjes van twee blaadjes.
- dinosaurus: dikkere poten, lava op de kraterrand in plaats van in de kegel.
- vlinder: acht symmetrische zonnestralen in plaats van vijf scheve.
- rechte kadelijnen in molen, bergkamp, kasteel, dinosaurusvallei, tuindieren
  en vierseizoenen zijn golvend gemaakt; ze lazen als een liniaal.
- leven rond de vijver had een lege bovenhelft: zon, wolk en zwaluwen erbij.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 11:29:43 +02:00
Ramon
c6b3a14923 fix: maak kleurplaatverf zichtbaar en herteken de atlas (v0.4.75-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
Kwast en verfemmer werkten al, maar het resultaat was onzichtbaar: de
contourlaag zette fill="#fff" op elk vak en vertrouwde op
mix-blend-mode:multiply om dat onschadelijk te maken. Doordat .coloring-ink
zelf een stacking context is (position+z-index), vormt die een geisoleerde
blendgroep zonder achtergrond, en werd het wit gewoon dekkend over de hele
tekening gelegd. Live nagemeten met headless Chromium.

- Tekenmodel omgezet naar een echte schildersvolgorde in een enkele SVG: elk
  vak wordt uitgeklapt naar vak -> verflaag -> contour, zodat verf zichtbaar
  blijft en latere vakken de verf en contour van eerdere vakken afdekken.
  Daarmee kijk je ook niet meer door figuren heen.
- Canvas vervangen door SVG-streken: geen opschalingsonscherpte meer en geen
  donkere naden (dekking staat per streek op de groep, niet per segment).
- Kwast blijft binnen de lijnen als hulp, gestuurd door niveau: groep 1-4 op
  niveau 1 en 2, groep 5-8 op niveau 1, daarboven vrij schilderen.
- Verfemmer dekt een vak volledig; de gum pelt de bovenste laag af (eerst de
  vulling, daarna de streken eronder).
- Kleurwijzer toegevoegd: op niveau 1 laat een stip per vak de voorgestelde
  kleur zien, op niveau 2 op te vragen met een knop, op niveau 3 uit.
- Niveau sloopt de tekening niet meer: data-tier staat alleen op groepen en
  detaillijnen, dus een rekwisiet verdwijnt als geheel en de plaat is op elk
  niveau een complete tekening. Niveau 3 voegt alleen fijn lijnwerk toe.
- Atlas hertekend naar 20 platen, 5 per groepenband en 1 per thema. Twee
  lijndiktes, vakken op maat van de band, geen evenodd-gaten, en composities
  met voor-, midden- en achtergrond.
- partBelow/elementFromPoint en het omzetten van pointer-events vervallen:
  aanwijzen gebeurt meetkundig met isPointInFill/isPointInStroke.
- langchange-listener ruimt zichzelf op na demontage.
- Staat bewaart nu streken in plaats van een PNG; oud werk met een ink-PNG
  blijft zichtbaar.
- Tests: asserties die de bug vastzetten verwijderd en vervangen door een
  regressietest die verbiedt dat CSS een fill op een kleurvak zet, plus
  controles op gesloten vakken, geen transform, tier-regels, vakaantallen per
  niveau en de clip-matrix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
2026-07-30 00:39:14 +02:00
Ramon
66d6cf4d46 feat: bouw kleurplatenatlas opnieuw op (v0.4.74-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
- Teken alle 28 scènes vanaf een leeg canvas opnieuw met samenhangende silhouetten en detailniveaus per leerjaar.
- Geef dieren soortspecifieke anatomie, natuurlijke verhoudingen en vloeiende realistische contouren.
- Borg gesloten vulvakken, correcte laagvolgorde en blijvend zichtbare contour- en binnenlijnen.
- Verwijder de oude reparatielaag en voeg anatomische en functionele regressietests toe.
2026-07-29 16:59:27 +02:00
Ramon
6a08cee75c feat: herteken kleurplaten als illustraties (v0.4.73-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
- vervang alle 28 kleurplaten door samenhangende scènes met organische contouren en leeftijdsgerichte detaillering
- houd buiten- en binnenlijnen zichtbaar boven kwastwerk en sluit verfemmerkleuren zonder witte rand aan
- borg gesloten kleurvakken, anatomische laagvolgorde en scene-opbouw met uitgebreide regressietests
2026-07-29 15:44:38 +02:00
Ramon
03bfb9feda fix: herstel gum en kleurplaatlagen (v0.4.72-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- laat de gum ook verfpotvullingen bij tikken en slepen verwijderen
- vervang dubbele filterlagen door één gemaskeerde contourlaag zonder witte halo
- verberg overlappende herkenningshulplijnen en dek achterliggende contouren correct af
- voeg regressietests toe voor gum, vulranden en lijnlagen
2026-07-29 14:49:34 +02:00
Ramon
724eb6a33c feat: herontwerp kleurplaten professioneel (v0.4.71-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 40s
Gewijzigde functionaliteiten:
- Vervang losse geometrische dierdelen door vloeiende Bézier-silhouetten met geloofwaardiger anatomie.
- Schilder poten, staarten, vinnen, vleugels, wielen en andere appendages achter het hoofdsilhouet voor aansluitende contouren.
- Voeg een SVG-occlusiefilter toe dat alleen werkelijk zichtbare lijnen boven kwast en verfpot toont.
- Verfijn lijngewichten, witte contrastrand, wielkasten, manen en objectcontouren.
- Geef kwast, verfpot en gum zichtbare labels en verbeter ARIA-groepering van gereedschappen en palet.
- Voeg regressietests toe voor organische contouren, schildervolgorde, gesloten kleurvakken en UX-bediening.

Tests:
- npm test (201 geslaagd)
2026-07-29 14:27:26 +02:00
Ramon
33398c8594 fix: herstel kleurplaatgereedschappen (v0.4.70-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
Gewijzigde functionaliteiten:
- Vervang foutgevoelige zwarte en witte SVG-vullingen door transparante contourlagen.
- Laat de verfpot rechtstreeks de aanklikbare kleurvakken bedienen.
- Vervang het kleurpotlood door een vloeiende, pendrukgevoelige kwast met eigen icoon en cursor.
- Migreer opgeslagen potloodstate automatisch naar het nieuwe kwastgereedschap.
- Voeg regressietests toe voor achtergrond, laagweergave en gereedschapsinvoer.

Tests:
- npm test (201 geslaagd)
2026-07-29 14:05:57 +02:00
Ramon
979f30a935 feat: geef kleurplaten een grafische upgrade (v0.4.69-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
Gewijzigde functionaliteiten:
- Splits kleurplaten in kleur-, potlood- en bovenste inktlaag zodat contouren zichtbaar blijven.
- Versterk lijnen met een lichte contrastrand en donkere inkt, met behoud van de juiste overlappingen.
- Vernieuw werkvlak, gereedschappen en kleurenpalet voor een krachtiger grafische uitstraling.
- Voeg regressietests toe voor laagvolgorde en inktweergave.

Tests:
- npm test (201 geslaagd)
2026-07-29 10:18:59 +02:00
Ramon
1e0f0c6d03 feat: verfijn grafische kwaliteit kleurplaten (v0.4.68-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
Gewijzigde functionaliteiten:

- Brengt een rustige lijnhiërarchie aan met subtiele buitenkaders, stevige kleurvlakken en lichtere binnenlijnen.
- Voegt herkenbare textuurdetails toe aan onder meer vlinder, herfstboom, taart, zeedieren, molen, raket en kasteel.
- Verbetert het fietsframe in de duurzame-stadplaat en markeert kleurvlakken bij aanwijzen met de verfemmer.
- Borgt het consistente SVG-profiel voor alle 28 kleurplaten met aanvullende tests.

Tests: 201 geslaagd (npm test); 5 gerichte kleurplaattests geslaagd.
2026-07-29 08:37:32 +02:00
Ramon
3aafd50d62 Verbeter grafische kwaliteit kleurplaten
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
2026-07-29 08:02:53 +02:00
Ramon
24816c0d64 feat: breid kleurplaten uit en verfijn details (v0.4.66-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 42s
2026-07-28 23:05:49 +02:00
Ramon
0841962824 feat: voeg thematische kleurplaten toe (v0.4.65-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 44s
2026-07-28 22:34:15 +02:00
Ramon
fcb117eb54 fix: lijn het stippenpatroon uit binnen elk blokformaat (v0.4.64-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 50s
Gewijzigde functionaliteiten:

- De open stippen in de doelvakken stonden links-boven in hun rastercel in
  plaats van gecentreerd, waardoor het patroon scheef oogde; ze staan nu op
  dezelfde as als de gevulde stippen.

- De stippen van de geldblokken en van alle cijferblokken in een smalgemaakte
  widget staken onder het blok uit. De stipmaat komt nu uit een variabele die
  elk bloktype naast zijn eigen hoogte zet, in plaats van uit losse
  hardgecodeerde regels per widget.

- De smalle-widget-weergave verkleint alleen nog de breedte; blokhoogtes
  blijven gelijk, want juist het verkleinen van de hoogte duwde het raster naar
  buiten. Letterblokken krijgen daar een passend kleiner cijfer/letterformaat.

- Nieuw vangnet: het raster houdt altijd zijn maat en het label geeft mee als
  een blokformaat krap is, zodat een nieuw blokformaat dit niet opnieuw kan
  veroorzaken.

Tests: 196 geslaagd (4 nieuw in test/dot-grid.test.js; die vier vallen ook
daadwerkelijk om op de oude CSS). Daarnaast in de browser opgemeten voor alle
zeven blokcontexten, bij normale en smalle widgetbreedte, plus screenshots en
een controle in de echte leerlingweergave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126hjBPD2ggZ2LEoDYZSW7S
2026-07-26 08:13:02 +02:00
Ramon
50b9f32e91 feat: voeg voortgangdetail toe aan de taalwidgets (v0.4.63-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m56s
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
2026-07-26 03:16:28 +02:00
Ramon
043831e1dd feat: make assignments independent from boards
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m28s
2026-07-25 22:15:52 +02:00
Ramon
05f3dbe56c feat: simplify assignment sequence sharing
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
2026-07-24 23:08:44 +02:00
Ramon
b8e9e46819 fix: patch Fastify static security vulnerabilities
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m20s
2026-07-24 22:46:22 +02:00
Ramon
a7180d8390 feat: combine assignments and class management
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 56s
2026-07-24 22:36:15 +02:00
Ramon
131ffc2f38 feat: breid live klasinteractie grafisch uit
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 47s
2026-07-24 13:40:18 +02:00
Ramon
90851f960a feat: voeg live leersessies toe aan klasdashboard
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m38s
2026-07-24 10:53:42 +02:00
Ramon
29b12e6c81 fix: verhelp hoge find-my-way kwetsbaarheid
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m17s
2026-07-23 23:22:03 +02:00
Ramon
b2f6a69897 fix: maak leerkrachtfuncties zichtbaar in voorbeeldmodus
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 1m10s
2026-07-23 23:15:08 +02:00
Ramon
ae8d0d4bb4 feat: voeg leerpaden dashboard en avatarbeloningen toe
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m23s
2026-07-23 21:23:54 +02:00
Ramon
9dc896964d fix: maak mobiele opdrachten volledig zichtbaar
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m11s
2026-07-23 19:05:53 +02:00
Ramon
ff6dbc5c2e fix: voorkom witte onderstrook op mobiel
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 53s
2026-07-23 17:34:47 +02:00
Ramon
c33eb71640 fix: centreer letter- en cijferdoelvakken
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m4s
Gewijzigde functionaliteiten:

- Centreert doelvakken in letter-, reken-, klok-, geld-, plaatswaarde- en wereldwidgets.

- Houdt lange woordrijen scrollbaar zonder de eerste vakken af te snijden.

- Centreert kleinere geplaatste cijferblokken binnen hun doelvak.

- Verwijdert de links uitgelijnde compacte woordrij.

Tests: 160 geslaagd.
2026-07-23 17:06:26 +02:00
Ramon
34c4d6e1a4 feat: laat avatars dansen na afgeronde opdrachten
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 42s
Gewijzigde functionaliteiten:

- Voegt vijf afwisselende avatardansen toe.

- Houdt lichaam, haar en accessoires als een verbonden SVG-figuur.

- Danst na afgeronde woordgroepen, flitsrondes, galgjerondes en rekenrondes.

- Respecteert verminderde beweging met een stil feestbeeld.

Tests: 159 geslaagd; npm audit: 0 kwetsbaarheden.
2026-07-23 07:07:30 +02:00
Ramon
4544306ccd feat: voeg avatarsoortvarianten toe (v0.4.50-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m8s
2026-07-22 23:40:36 +02:00
Ramon
2b40488208 fix: patch fast-uri kwetsbaarheid (v0.4.49-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m15s
2026-07-22 23:21:23 +02:00
Ramon
6832043470 feat: maak avatarhaar natuurlijker (v0.4.48-beta)
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 43s
2026-07-22 23:13:20 +02:00
Ramon
7237cb6290 feat: breid compacte avatars uit (v0.4.47-beta)
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 35s
2026-07-22 19:59:15 +02:00
Ramon
d0522d7e33 feat: verbeter anatomie en accessoires van avatars (v0.4.46-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 58s
2026-07-21 23:25:12 +02:00
Ramon
a922caaa56 fix: behoud groep 1 beeldankers voor letters
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 44s
2026-07-21 22:29:24 +02:00
Ramon
dc3c591c76 feat: stem taalwidgets af op beginnende geletterdheid
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m5s
2026-07-21 22:27:44 +02:00
Ramon
6f593e05b2 fix: corrigeer woordplaatjes in taalbibliotheek (v0.4.44-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m1s
2026-07-21 22:01:41 +02:00
Ramon
ba4483d5d3 feat: breid taalbibliotheek uit met 1000 woorden (v0.4.43-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 59s
2026-07-21 18:50:47 +02:00
Ramon
17ee47dd09 fix: maak bordniveaus instelbaar (v0.4.42-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 42s
2026-07-21 07:58:56 +02:00
Ramon
fabe721991 feat: voeg leerjaarprofielen toe aan taal en rekenen (v0.4.41-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
2026-07-20 22:45:01 +02:00
Ramon
629329894d feat: laat systeemmanager leerlingwidgets toevoegen (v0.4.40-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 53s
2026-07-20 16:06:33 +02:00
Ramon
b81f5ac5dd feat: voeg rolvoorbeeld voor systeemmanager toe (v0.4.39-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
2026-07-20 14:08:37 +02:00
Ramon
9bab7f39c0 feat: koppel widgets aan kerndoelen (v0.4.38-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
2026-07-19 21:46:38 +02:00
Ramon
322147d3d1 feat: voeg curriculumwiki voor volwassenen toe (v0.4.37-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m1s
2026-07-19 21:13:11 +02:00
Ramon
216ab8d5dd docs: leg SLO-kerndoelen 2026 vast (v0.4.36-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
2026-07-19 20:58:17 +02:00
Ramon
8ccfa5dc8c feat: voortgang flinke UI-upgrade + bug (1 opdracht i.p.v. groep) + grafiek (v0.4.35-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m17s
- 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
2026-07-19 15:56:14 +02:00
Ramon
e7bb76b76d feat: klassenmanagement - niveaugroep + niveau per leerling per vakgebied (v0.4.34-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m9s
- 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
2026-07-19 15:31:26 +02:00
Ramon
36695a06f6 fix: leerling in werken-modus kreeg abusievelijk readonly:true (v0.4.33-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 46s
- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 14:39:00 +02:00
Ramon
643a00c1c3 feat: personage-aanmoediging uitgebreid naar klok, geld, plaatswaarde en spellen (v0.4.32-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
- De math.js-pilot (v0.4.23-beta: eigen personage kort naast "Goed!" bij
  een juist antwoord) hergebruikt in vier andere widgets, zoals destijds
  al voorzien in de roadmap: klok (mountClock), geld (mountMoney),
  plaatswaarde (mountPlacevalue) en de wereldoriëntatie-spelopdracht
  (mountWorldQuiz - dekt bomen/vogels/planten in één keer - en
  mountTopography).
- Zelfde patroon overal: bij S.feedback==='good' (of S.spellSolved) en
  een ingesteld personage verschijnt renderAvatarFace(...,"md") met de
  av-cheer-animatie naast het bestaande feedbackbericht - puur visueel,
  geen nieuw feedbacktype. Zonder personage blijft het gedrag exact
  ongewijzigd.
- Geverifieerd met echte oplos-interacties (niet alleen mounten): klok
  (typemodus + toetsenbord), geld (typemodus + toetsenbord), plaatswaarde
  (blokken plaatsen via toetsenbord-Enter) en de spelopdracht (letters
  plaatsen via toetsenbord-Enter) - in alle vier verschijnt het
  personage daadwerkelijk in de feedback-HTML na een echt juist
  antwoord. Volledige testsuite: 112/112.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 14:03:58 +02:00
Ramon
c183c8f931 perf: opstart-fetches /auth/me en /me/data parallel i.p.v. serieel (v0.4.31-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 37s
- Beide verzoeken hangen alleen af van dezelfde sessie-cookie, niet van
  elkaars antwoord - ze werden tot nu toe na elkaar aangeroepen
  (hydrateFromServer() startte pas na een voltooide /auth/me), wat een
  hele round-trip extra kost bij elke opstart voor staf/thuisgebruikers.
- app.js vraagt ze nu tegelijk op via Promise.allSettled; hydrateFromServer()
  krijgt het /me/data-resultaat optioneel meegegeven i.p.v. het zelf
  (opnieuw) op te halen. Voor een leerling/ouder wordt dat resultaat
  genegeerd (zij krijgen sowieso geen whiteboard-data) - nooit verspild
  werk, soms ongebruikt, wat de server niet meer kost dan de bestaande
  losse aanroep die er toch al was.
- De twee andere hydrateFromServer()-aanroepen (na inloggen, na een
  save-conflict) geven bewust niets mee en halen gewoon vers op, zoals
  voorheen - alleen de allereerste opstart kan dit parallelliseren.
- Geverifieerd met de volledige testsuite (112/112) en drie losse
  boot-scenario's in de sandbox (gast/staf/leerling): in alle drie komen
  /auth/me en /me/data binnen ~1ms van elkaar aan (voorheen serieel), en
  blijft het bestaande gedrag (gast blijft gast, staf krijgt het bord,
  leerling krijgt de afgeschermde weergave) exact hetzelfde.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 13:43:32 +02:00
Ramon
4e4aeb4b6d feat: exporteren van vers-gegenereerde wachtwoorden als CSV (v0.4.30-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
- "Exporteer als CSV"-knop toegevoegd aan het bestaande multireset-
  resultatenscherm (klas-wachtwoorden resetten in de School-navigator),
  naast de bestaande "kopieer alles"-knop.
- BELANGRIJKE BEPERKING, bewust zo: dit exporteert alleen wat op dit
  scherm al staat (net-gegenereerde wachtwoorden). Wachtwoorden worden
  na het zetten nergens leesbaar bewaard (alleen gehashet in de
  database) - er is dus geen "haal wachtwoorden van bestaande accounts
  op"-mogelijkheid en die komt er ook niet.
- CSV bevat gebruikersnaam (het echte inlogveld) naast de naam, ook al
  toont het scherm zelf alleen de naam - zonder gebruikersnaam is een
  uitdeelvel niet bruikbaar om mee in te loggen.
- Hergebruikt de csvValue()/downloadCsv()-helpers uit de vorige
  resultaten-exportfeature (v0.4.29-beta) - eerste echte tweede
  gebruiker, dus die keuze om ze lokaal in admin.js te houden i.p.v.
  te verplaatsen was terecht.
- Geverifieerd met de volledige testsuite (112/112) en een live
  browserdoorloop: klas-wachtwoorden resetten (dubbele bevestigingsklik)
  → CSV-download met correcte escaping van een leerlingnaam met komma
  én aanhalingsteken.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 13:31:10 +02:00
Ramon
8fe6fc10cc feat: exporteren van resultaten per klas (CSV) (v0.4.29-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 51s
- 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
2026-07-19 12:54:48 +02:00
Ramon
09a1326c7e feat: meer personage-accessoires incl. zonnebril (v0.4.28-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m10s
- 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
2026-07-19 11:55:25 +02:00
Ramon
817f12b70d fix: dino-rugstekels en beer-staart oogden vreemd (v0.4.27-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- Dino: rugstekels stonden als een rij spitse vormen voor de hals/borst
  ("Ga als designer kijken" - gebruiker vond dit "wat vreemd"), wat als
  een rare kraag oogde. Verplaatst naar een klein randje bovenop de kop
  (zoals kleine hoorntjes) - dichter bij de anatomische plek (rug/kruin)
  en rustiger op klein formaat.
- Dier (beer): had een los staartje opzij op dezelfde hoogte en met
  vrijwel dezelfde vorm/grootte als de armen/benen, waardoor het als een
  extra ledemaat oogde. Verwijderd - een beer laat van de voorkant
  sowieso nauwelijks iets van zijn staart zien, dus dit is ook
  anatomisch kloppender dan een zichtbaar zij-staartje.
- Overwogen: SVG vs. PNG-lagen als basis. SVG blijft de juiste keuze
  hier - lichaams-/haarkleur zijn vrije kleurkiezers (elke hex-waarde),
  wat via fill-attributen triviaal is maar bij PNG een vast palet of
  canvas-hertintplugin zou vereisen; SVG schaalt bovendien scherp op
  alle drie weergavegroottes zonder aparte assets.
- Geverifieerd: alle vijf bestaande scenario's, volledige testsuite,
  en een School-navigator met een echte klas (leerling met dier-
  personage toont nu geen vreemd zij-staartje meer).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 11:44:55 +02:00
Ramon
46696b0d7d refactor: personage-tekening volledig herbouwd als SVG (v0.4.26-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m40s
- Kernprobleem: de CSS-laagjesversie (losse divs met percentage-posities
  en z-index) had onderdelen die los van het lichaam hingen en een
  tekenvolgorde die niet klopte (soort-specifieke delen soms achter
  hoofd/lijf verstopt, soms er los voor zwevend). SVG lost dit
  structureel op: tekenvolgorde = laagvolgorde, geen z-index nodig, en
  elk onderdeel is een gebogen path/ellips die het silhouet van het
  vorige onderdeel ruim overlapt in plaats van ernaast te zweven.
- Onderzocht hoe gangbare avatar-generators (DiceBear, Avataaars) dit
  aanpakken: vaste 100x100 viewBox, onderdelen als SVG-groepen in de
  juiste volgorde, kleur via directe fill-waarden i.p.v. CSS-variabelen
  in SVG. Dat patroon hier toegepast, geen code overgenomen.
- Elke soort (dino/monster/dier) is nu een samenhangend lichaam: poten,
  armen, staart/oren, buikvlek en gezicht overlappen allemaal duidelijk
  met het hoofd/lijf. Dino: rugstekels, snuit met neusgaten, glimlach.
  Monster: hoorntjes, brede mond met slagtandjes, asymmetrische ogen.
  Dier: ronde oren, beer-snuit, kleine staart.
- Haar (spits/krullen/paardenstaart/mohawk) en accessoires (pet/kroon/
  bril/strik/sjaal/cape/badge) opnieuw getekend zodat ze ook echt op
  hun naam lijken, i.p.v. simpele gekleurde blokjes.
- Regel toegevoegd: een pet of kroon dekt het hele bovenhoofd af, dus
  onderliggend haar wordt dan niet getekend (gaf anders rommelige,
  half verstopte plukjes rond de rand) - een paardenstaart blijft wel
  zichtbaar, want die hangt naar opzij.
- Kleuren (lichter voor buik/snuit, donkerder voor poten/staart/
  rugstekels) worden nu berekend uit de gekozen lichaamskleur zelf
  (avatarShade-helper), i.p.v. via CSS filter:brightness().
- CSS: alle oude .av-* laag-regels (~65 regels) verwijderd; alleen
  afmetingen (sm/md/lg) en de bestaande instellingen/widget/
  aanmoedigings-CSS blijven staan.
- Geverifieerd: alle vijf bestaande scenario's (opslaan, widget,
  School-navigator-badge met een echte klas vol verschillende
  personages, math.js-aanmoediging, ongeldige input), plus uitgebreide
  screenshot-rondes van losse onderdelen, alle soort/haar/accessoire-
  combinaties en alle drie weergavegroottes (24/44/180px) vóór het
  committen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 10:19:19 +02:00
Ramon
4438fad52b fix: dino-personage krijgt een echt dinolichaam i.p.v. een blote romp (v0.4.25-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m5s
- Buikvlek (lichter gekleurde ovaal) op de romp - een klassiek dino-kenmerk
  dat meteen de "kleurvlek zonder vorm"-indruk wegneemt.
- Twee kleine pootjes onderaan en twee stompe armpjes opzij (T-rex-stijl) -
  het lichaam stond eerder als een rond blok zonder ledematen.
- Staart herzien naar een echte taps toelopende vorm (clip-path) i.p.v. de
  ronde druppelvorm van hiervoor.
- Alleen voor soort "dino" - monster/dier ongewijzigd, geen regressie
  (gecontroleerd via de screenshot-galerij van alle drie soorten samen).
- Geverifieerd op alle vijf bestaande scenario's (opslaan, widget,
  School-navigator-badge, math.js-aanmoediging, ongeldige input) én
  opnieuw op alle drie weergavegroottes (24/44/180px) vóór het committen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 09:57:48 +02:00
Ramon
9bf91f0bd1 fix: personage-tekening had een gezicht noch duidelijke dino-vorm (v0.4.24-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- Elk personage kreeg ogen (was volledig gezichtsloos - de kernoorzaak van de
  lage beeldkwaliteit: een gekleurde vlek zonder gezicht oogt nooit als een
  figuur, ongeacht soort/haar/accessoires).
- Dino kreeg een snuitje met neusgaten, een duidelijk zichtbaar staartje en
  een derde, groter/donkerder rugstekel-plaatje - hiervoor waren de
  rugstekels aanwezig maar onzichtbaar (z-index onder het lijf/hoofd, zie
  vorige commit), nu bewust vóór het silhouet getekend zodat ze altijd tonen.
- Snuitje stond eerst z-index-onder de rugstekels (dus zelf ook onzichtbaar,
  identieke fout als hierboven) - z-index verhoogd zodat het snuitje altijd
  vóór de rugstekels ligt, wat ook anatomisch klopt (snuit zit vooraan).
- Geverifieerd met alle vijf scenario's uit de vorige verificatie (opslaan,
  widget, School-navigator-badge, math.js-aanmoediging) plus een losse
  screenshot-galerij op alle drie weergavegroottes (24/44/180px).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ
2026-07-19 09:50:18 +02:00
Ramon
24d89b8bb5 feat: eigen personage (avatar) voor leerlingen, ouders en staf (v0.4.23-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- 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
2026-07-19 07:34:25 +02:00
Ramon
8b57922307 fix: schoolbeheerder zichtbaar als groepsleiding + A+-knop verspringt niet meer (v0.4.22-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- Een schoolbeheerder (of systeemmanager) die aan een klas gekoppeld werd,
  kwam daar niet zichtbaar te staan: de klas-detailweergave en de
  staf-telling op de klastegel filterden op role==="teacher", terwijl de
  server al sinds v0.3.38 elke stafrol (teacher/admin/super) toestaat in
  class_teachers. Puur een weergavebug op de client - de koppeling zelf
  werkte al goed. Nieuwe isClassStaff()-helper matcht nu exact de
  serverkant se toegestane rollenset.
- De "+" in de A+-zoomknop (toolbar én het per-widget contentzoom-knopje)
  gebruikte de CJK-volle-breedte "+" (U+FF0B) naast de gewone letter "A" -
  dat teken valt vermoedelijk terug op een ander lettertype dan Quicksand,
  waardoor de relatieve positie t.o.v. de "A" verspringt zodra
  document.body.style.zoom verandert. Vervangen door de gewone ASCII "+"
  (net als de A−-knop al een normaal minteken gebruikt, niet de volle-
  breedte variant).
2026-07-19 01:06:39 +02:00
Ramon
9920fd3855 feat: vier sociaal-emotionele widgets, waaronder een geluidsmeter (v0.4.21-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- Geluidsmeter: meet het klasgeluid live via de microfoon in een verticale
  meter (rustig/pratend/te luid). Twee aanpasbare streefniveaus ("Rustig
  tot" / "Te luid vanaf") laten de leerkracht de meter afstemmen op het
  eigen lokaal en apparaat. Microfoon staat standaard uit en start alleen
  na een expliciete klik; er wordt niets opgenomen of verstuurd, alles
  blijft lokaal in de browser. Stopt de microfoonstream vanzelf zodra de
  widget van het bord verdwijnt, ook als "Stop" niet eerst is ingedrukt.
- Gevoelszones: individuele check-in met vier zones (blauw/groen/geel/
  rood, Zones of Regulation-stijl) - elke zone geeft een korte uitleg én
  een concrete tip om jezelf te helpen. Anders dan de bestaande
  Gevoelsmeter (mood.js), die een anonieme klaspoll is.
- Ademhalingsoefening: een groeiende/krimpende cirkel begeleidt rustig
  ademhalen in drie instelbare tempo's (4-4, 4-7-8, box breathing),
  met rondjesteller.
- Complimentenmachine: trekt een willekeurig compliment uit twintig
  kindvriendelijke complimenten, met dezelfde spin-en-onthul-animatie als
  de namenkiezer.
- Alle vier volledig tweetalig (nl/en), toegevoegd aan de mobiele-dekking-
  test, en geverifieerd in de browser inclusief een echte (fake) micro-
  foonstream voor de geluidsmeter.
2026-07-19 00:44:39 +02:00
Ramon
63490719d6 fix: letterblokken bij wereldoriëntatie verschillen nu ook per niveau (v0.4.20-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 51s
- De nieuwe "Letterblokken"-werkvorm (v0.4.18) verklapte op elk niveau
  altijd elke letter - alleen de soorten-/plaatsenpool werd moeilijker,
  de spelopdracht zelf bleef identiek. Bij de originele letterblokken-
  widget (letters.js) maakt niveau juist de opdracht zelf moeilijker:
  niveau 1 verklapt alles, niveau 3 niets.
- Zelfde schaal nu ook hier: niveau 1 toont elke letter, niveau 2 ongeveer
  de helft (willekeurig gekozen, vast per opdracht), niveau 3 toont geen
  enkele letter. Het puntenpatroon per vak blijft op elk niveau zichtbaar
  als tastbare hint, ook wanneer de letter zelf verborgen is.
- Geverifieerd: niveau 1 toont 11/11 letters, niveau 2 ~helft, niveau 3
  0/13 (met de puntenpatronen nog intact).
2026-07-19 00:23:39 +02:00
Ramon
705bb7800a fix: niveaus bij wereldoriëntatie nu als knoppenrij, net als de andere oefenwidgets (v0.4.19-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
- Bomen/vogels/planten/topografie verstopten "Niveau" als een los
  keuzeveld onderin het inklapbare Instellingen-paneel; de oefenklok,
  het geldwidget, plaatswaarde en rekenwidgets tonen niveau altijd als
  een knoppenrij bovenaan, direct zichtbaar tijdens het spelen.
- Niveau is nu overal dezelfde knoppenrij (Niveau 1/2/3, met uitleg per
  knop via title, net als bij de oefenklok) direct in de bovenbalk naast
  de score - geen Instellingen meer nodig om niveau te wisselen.
- Niveau wisselen zet score en reeks terug naar 0, zoals bij de andere
  oefenwidgets (voorheen bleef de score gewoon doorlopen).
- Topografie: de knoppenrij verdwijnt bij "Landsdelen", waar niveau toch
  al niet van toepassing was - zelfde gedrag als voorheen, nu alleen
  consistent met de rest.
2026-07-19 00:16:09 +02:00
Ramon
5cdc854e69 feat: flitskaarten en letterblokken bij wereldoriëntatie (v0.4.18-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 37s
- Bomen, vogels en wilde planten krijgen twee nieuwe werkvormen naast
  Leren/Quiz: Flitskaarten (naam kort tonen, dan zelftoets - zelfde opzet
  als het losse Flitswoorden-widget) en Letterblokken (de naam met
  sleepblokken spellen, zelfde mechaniek als de letterblokken-widget,
  inclusief puntenpatroon per letter).
- Topografie krijgt dezelfde twee werkvormen: flitskaarten tonen de kaart
  met de plek pas gelabeld na onthullen, letterblokken laat de naam van de
  gemarkeerde provincie/plaats/rivier/werelddeel spellen.
- Letterblokken werkt ook bij namen met spaties (bv. "Witte
  paardenkastanje") - spaties worden een visuele tussenruimte, geen vak.
- Geverifieerd door alle vier widgets (bomen/vogels/planten/topografie)
  volledig te spelen: flitskaart onthullen + zelftoets, en een woord
  volledig spellen puur op basis van de getoonde puntenpatronen.
2026-07-18 19:35:08 +02:00
Ramon
daad79c8b9 fix: vakken tonen nu het puntenpatroon van het júíste cijfer, zoals bij letterblokken (v0.4.17-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
- De vorige puntenpatroon-fix (v0.4.16) loste alleen op dat de tien
  cijferblokken onderling verschillen. De écht ontbrekende stap: bij de
  letterblokken en de rekenwidgets (madd/msub/mmul/mdiv/mpct) toont een
  leeg vakje altijd het puntenpatroon van het cijfer/de letter die er
  hoort te staan - een tastbare hint, ongeacht wat er al ligt. Bij de
  oefenklok en het geldwidget toonde het vakje in plaats daarvan niets
  (leeg) of het patroon van wat er toevallig al in lag - geen hint.
- Oefenklok (blockAnswer), geldwidget (digitAnswer) en de nieuwe
  plaatswaarde-oefening (buildAnswer) tonen nu overal het patroon van het
  juiste doelcijfer per vak, consistent met letterblokken/rekenwidgets.
- Geverifieerd door drie oefeningen (geldrekenen niveau 2/3, oefenklok
  niveau 3, plaatswaarde niveau 3) volledig op te lossen met uitsluitend
  de puntenpatronen als leidraad (geen tekst gelezen) - in alle gevallen
  "Goed gedaan!".
2026-07-18 16:33:07 +02:00
Ramon
bab1018142 fix: cijferblokken kregen elk een eigen puntenpatroon + niveaus voor plaatswaarde (v0.4.16-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- De cijferblokken (oefenklok, geldwidget) hergebruikten de braille a-j-
  patronen, die nooit de onderste rij (posities 3/6) gebruiken - daardoor
  leken alle tien blokken op elkaar. Eigen DIGIT_DOTCODE-tabel die alle
  zes stippen inzet, geeft elk cijfer nu een echt eigen vorm.
- Plaatswaardetabel-widget had geen enkele oefenvorm (alleen een vrij
  invoerveld); nieuwe "Bouwen"-stand met drie niveaus (T/E, H/T/E, of
  HD/TD/D/H/T/E) laat kinderen een getal opbouwen door cijferblokken in
  de juiste kolom te slepen, met score/reeks en directe feedback - zelfde
  bouwsteen als de oefenklok/het geldwidget. De vrije verkenstand blijft
  ongewijzigd bestaan.
- Bijvangst: instellingenvelden met .hidden (bv. "24-uursklok" bij de
  oefenklok) werden nooit echt verborgen door een CSS-specificiteitsbotsing
  (.edu-controls label versloeg [hidden]); toegevoegde regel herstelt dit
  voor alle leerwidgets.
2026-07-18 15:46:24 +02:00
Ramon
e555876fa4 feat: cast bord/widget vanaf whiteboard naar klas of leerling (v0.4.15-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
- Nieuwe Cast-knop (📡) op de whiteboard-toolbar opent een sheet om het
  geopende bord - of één widget erop met kijken/werken - direct naar een
  klas of leerling uit te zenden, zonder om te weg via Instellingen.
- Hergebruikt de bestaande PUT/DELETE /assignments/class|pupil routes
  ongewijzigd; geen serverwijziging nodig.
- Instellingen > Toewijzingen blijft bestaan maar toont nu alleen de
  huidige toewijzing + een "Loskoppelen"-knop (aanmaken/wijzigen gebeurt
  voortaan via Cast).
2026-07-18 13:26:26 +02:00
Ramon
3c3638d114 feat: "School" herbouwd als drill-down navigator (v0.4.14-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
Volledige herbouw van het "School"-tabblad naar het goedgekeurde concept
(tegel-overzicht -> gefocust detailscherm), i.p.v. drie secties op één lange
pagina. Zelfde patroon op elk niveau:
- "alle scholen" (systeemmanager): tegel per school + Systeemmanagers/Ouders/
  Zonder school + "+ School"
- een school: tegel per klas + Schoolbeheerder/Niet ingedeeld/Ouders/
  Aanmeldingen + "+ Klas"; "⋯" toont schoolbeheer (naam/verwijderen, alleen
  systeemmanager)
- een klas: Groepsleiding en Leerlingen als twee eigen sublijstjes, elk met
  een eigen "+"; "⋯" toont klasbeheer (verplaatsen/resetten/verwijderen)
- een platte groep: één lijst, evt. met "+"

Personen en aanmeldingen openen nu in een sheet (bestaande userTools()/
requestTools() vrijwel ongewijzigd hergebruikt) i.p.v. een los detailpaneel.
Zoeken vult, op tegel-niveau, het scherm met een systeembrede resultatenlijst.
Een leerkracht (groepsleiding) blijft de eenvoudige platte leerlingenlijst
zien, ongewijzigd.

Vervallen (overbodig dankzij drill-down, elke lijst is al kort en gefocust):
het hele inklap-/inspring-mechanisme (group(), openGroups/closedGroups,
editToggle, classHeadRow, applyUserFilter, .am-classhead, .am-indent), het
oude master-detail-paneel (selectedUserKey/syncSelection/renderDetail), en de
generieke "Nieuwe gebruiker"-knop (elke tegel se eigen "+" dekt dit al,
rol+context-correct).

Geen serverwijziging - pure client-herbouw van navigatie/weergave.
Geverifieerd in een echte headless Chromium voor systeemmanager (root +
school + klas + sheet + "+Groepsleiding" koppelt nog echt), schoolbeheerder
(eigen school, geen schoolkiezer/kebab-op-schoolniveau) en groepsleiding
(platte lijst, ongewijzigd).
2026-07-18 09:47:16 +02:00
Ramon
8580e46f4c fix: School-menu opnieuw ontworpen naar één consistente visuele taal (v0.4.13-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m0s
Was rommelig/te veel niveaus door TE veel verschillende stijlen op dezelfde
pagina: kaarten (Scholen) naast platte rijen (Gebruikers), een teal-getint
klasrijtje, een grijs getint paneel om Scholen - vier stijlen die om
aandacht streden i.p.v. één samenhangend geheel.

- Scholen gebruikt nu dezelfde platte .am-list/.am-row-stijl als de
  klassen/gebruikers-boom eronder i.p.v. losse kaarten in een eigen paneel;
  het paneel (.am-panel) is weg
- de ingeklapte beheerderslijst per school is weg - die informatie stond al
  in Gebruikers (gegroepeerd per school bij "alle scholen"), dubbel tonen
  droeg alleen bij aan de herrie
- klasrij heeft geen eigen teal-kader meer (alleen de naam iets zwaarder);
  hiërarchie loopt nu via een lichte inspringing (.am-indent) voor
  Groepsleiding/Leerlingen en hun rijen/+-knoppen, niet via kleur
- nieuwe editToggle()-helper: gebruikers-, klas- en schoolrijen delen nu
  dezelfde "Bewerken"-knop-implementatie i.p.v. drie keer bijna-dezelfde
  code, met een eigen openSchoolTools-Set (schoolrol- en klas-id's kunnen
  samenvallen met gebruikers-id's)
- geverifieerd in een echte headless Chromium: Scholen en Gebruikers ogen nu
  als één systeem, klas-substructuur is duidelijk ingesprongen, en de
  bestaande functionaliteit (rename/delete/reset/koppelen) werkt nog
2026-07-18 06:16:37 +02:00
Ramon
8db67b9cd6 fix: overzicht en scheiding in de Scholen-sectie van School (v0.4.12-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m15s
De losse schoolkaarten stonden zonder eigen kader los in de pagina en liepen
zonder duidelijke grens over in de Gebruikers-sectie eronder - verwarrend
omdat dezelfde schoolnaam daar soms letterlijk direct weer opduikt (nu als
groepskop i.p.v. kaart).

- Scholen krijgt een eigen omkaderd paneel (.am-panel) dat alle schoolkaarten
  en "+ School" samenbindt tot één zichtbaar geheel
- duidelijke scheidingslijn + meer ruimte vóór de volgende sectiekop, maar
  alleen ná een echte sectie (geen ongewenste lijn boven Gebruikers wanneer
  Scholen niet getoond wordt, bv. bij een gekozen school)
- geverifieerd in een echte headless Chromium: zowel de "alle scholen"-
  weergave (met paneel) als de weergave voor een gekozen school (zonder,
  geen regressie)
2026-07-18 05:46:26 +02:00
Ramon
5b4c3163a5 feat: Klassen en Gebruikers samengevoegd tot één boom per school (v0.4.11-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 51s
- klas, Groepsleiding en Leerlingen staan nu letterlijk bij elkaar: elke klas
  krijgt een eigen rij (naam + "Bewerken" voor verplaatsen/resetten/
  verwijderen, overgenomen uit het oude losse Klassen-paneel) met daaronder
  twee eigen sublijstjes - Groepsleiding (leerkrachten gekoppeld aan die
  klas) en Leerlingen - elk met een eigen "+"-knop
- "+ Groepsleiding" bij een klas maakt de nieuwe leerkracht niet alleen aan,
  maar koppelt die ook meteen aan die klas (hergebruikt de bestaande, al
  geteste POST /admin/classes/:id/teachers-route - geen serverwijziging)
- Schoolbeheerder blijft een eigen, vlakke groep los van de klassenboom (ook
  als een schoolbeheerder toevallig aan een klas gekoppeld is, om dubbele
  vermeldingen te voorkomen); Niet ingedeeld bundelt leerkrachten zonder klas
  en leerlingen zonder klas
- renderKlassenPanel is verwijderd (zijn inhoud leeft nu in de klas-tak van
  renderGebruikersPanel); de losse "Klassen"-kop in het School-tabblad is weg
- nieuwe openClassTools-Set voor het in-/uitklappen van klasbeheer, apart van
  openRows (klas- en gebruiker-id's kunnen samenvallen)
- geverifieerd in een echte headless Chromium: klasrij + twee sublijsten per
  klas, Schoolbeheerder/Niet ingedeeld correct gevuld, en "+ Groepsleiding"
  koppelt de nieuwe leerkracht daadwerkelijk aan de klas (classIds bevestigd)
2026-07-18 05:31:00 +02:00
Ramon
e088f83d1f fix: schoolkiezer reageert nu echt op Scholen/Klassen binnen "School" (v0.4.10-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- Scholen-blok toont alleen bij "alle scholen" (selSchool leeg), Klassen-blok
  alleen bij een gekozen school - voorheen toonde Scholen altijd, ongeacht de
  kiezer erboven, wat leek alsof het paneel de kiezer negeerde
- de kiezer defaultet voor de systeemmanager weer op "alle scholen" (was
  omgezet naar de eerste school in een eerdere commit; teruggedraaid op
  uitdrukkelijk verzoek)
- geverifieerd in een echte headless Chromium: bij "alle scholen" tonen
  Scholen+Gebruikers, bij een gekozen school Klassen+Gebruikers, en wisselen
  werkt in beide richtingen
2026-07-18 05:11:38 +02:00
Ramon
56125c6968 feat: Scholen, Klassen en Gebruikers samengevoegd tot één menu-item "School" (v0.4.09-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 40s
- ADMIN_SECTIONS: de drie losse tabbladen zijn samengevoegd tot 1 sectie
  ("school"); zoeken op een school-, klas- of gebruikersnaam werkt nog
  hetzelfde, navigeert nu alleen naar de ene samengevoegde sectie
- nieuwe renderSchoolPanel() zet de drie bestaande, ongewijzigde
  deelpanelen (renderScholenPanel/renderKlassenPanel/renderGebruikersPanel)
  na elkaar, elk met een eigen kop - geen van de drie is herschreven, alleen
  samengesteld, om het risico op regressies te beperken
- rolgedrag blijft exact zoals het was (nu allemaal binnen die ene sectie):
  systeemmanager ziet Scholen+Klassen+Gebruikers en de schoolkiezer,
  schoolbeheerder ziet Klassen+Gebruikers voor de eigen school (geen
  schoolkiezer, geen Scholen-blok), groepsleiding ziet alleen Gebruikers
  (geen Klassen-blok) en daarbinnen alleen de leerling-gerelateerde knoppen
- geverifieerd in een echte headless Chromium voor alle drie rollen: juiste
  secties, juiste knoppen, juiste zichtbaarheid van de schoolkiezer

Backup van de oude driedelige UI staat als git-tag
ui-before-merged-beheer-v0.4.08 (commit e34bda5).
2026-07-18 05:04:43 +02:00
Ramon
e34bda5251 test: expliciete regressietest voor de beperkte rechten van groepsleiding (v0.4.08-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 42s
Geen gedragswijziging - dit legt vast (en bevestigt) wat al klopte: een
groepsleiding kan geen rol wijzigen, geen gebruiker aan een school
koppelen/losmaken, geen klas aanmaken en geen school beheren. Alleen
systeemmanager (en voor klassen: ook schoolbeheerder) mag dit.
2026-07-18 04:49:10 +02:00
Ramon
e2ee4820dc fix: schoolbeheerder aan klas koppelen, systeemmanager-promotie dichten, UI-afstand (v0.4.07-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- 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.
2026-07-18 04:40:39 +02:00
Ramon
111e48b044 fix: toevoegformulier duidelijk anders dan een bestaande-gebruikersrij (v0.4.06-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
Een uitgeklapt "+ ..."-formulier (nieuwe schoolbeheerder/leerkracht/leerling/
school/klas) zag er in het beheerpaneel bijna hetzelfde uit als het
bewerkpaneel van een bestaande gebruiker - beide gewoon een rij velden
zonder duidelijk onderscheid.

- .am-add krijgt nu een eigen kader (accentkleur, stippellijn) i.p.v. de
  neutrale achtergrond van een gebruikersrij
- het formulier toont bovenaan een titel die herhaalt wát je toevoegt (bv.
  "LEERLING"), ook zodra de knoptekst erboven uit beeld is
- de dichtgeklapte "+ ..."-knop zelf krijgt ook de accentkleur, zodat
  collapsed en uitgeklapt dezelfde "dit is toevoegen"-taal spreken
- geverifieerd in een echte headless Chromium: duidelijk visueel onderscheid
  tussen een toevoegformulier en een echte gebruikersrij
2026-07-18 04:22:17 +02:00
Ramon
e5890280cc feat: snel-toevoegen-knop onder elke groep in Gebruikers (v0.4.05-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
Net als bij systeemmanagers (vorige commit) krijgt nu elke groep in de
Gebruikers-tab (schoolbeheerder, groepsleiding, en elke klas incl. "geen
klas") direct een eigen "+ ..."-knop eronder om iemand toe te voegen zonder
naar een ander tabblad te hoeven. Bij een klas staat die klas al vast
ingevuld (presetClassId, bestond al als parameter maar werd nog nergens
gebruikt). Een groep blijft zichtbaar zolang er iets aan toegevoegd kan
worden, ook als hij nu leeg is (bv. een gloednieuwe klas zonder leerlingen) -
anders was er nergens een knop om de eerste aan toe te voegen.

Geverifieerd in een echte headless Chromium (gemockte /api/** responses):
groepen en knoppen verschijnen zoals verwacht, en de klas-select bij een
klasgroep staat al goed vooringesteld.
2026-07-18 04:14:27 +02:00
Ramon
c6638d63f1 feat: systeemmanager aanmaken verplaatst naar Gebruikers + eigen wachtwoord ter bevestiging (v0.4.04-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- 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
2026-07-18 04:00:30 +02:00
Ramon
cae700f76d fix: leerling verplaatsen naar een andere klas ververst het paneel niet (v0.4.03-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
De klas-select bij een leerling (uitwisselen naar een andere klas) miste de
reload() die elders bij een klaswijziging wel gebeurt: de server sloeg de
nieuwe klas gewoon op, maar het paneel liet nog de oude toestand zien - leek
dus alsof verplaatsen niet werkte. Test toegevoegd die de serverkant van
deze route bevestigt (die werkte al correct); de fix zelf is puur de
ontbrekende reload() in public/js/admin.js.
2026-07-18 03:51:21 +02:00
Ramon
45f66044f2 fix: alle DELETE-verzoeken faalden op een lege JSON-body (v0.4.02-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
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)
2026-07-18 03:44:39 +02:00
Ramon
4517f592f7 chore: versienummering terug naar tweecijferig patchformaat (v0.4.01-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
0.3.99 rolde per ongeluk door naar 0.4.0/0.4.1 i.p.v. het gebruikelijke
tweecijferige patchformaat (0.4.00, 0.4.01, ...) - hersteld.
2026-07-18 03:40:51 +02:00
Ramon
6742e07363 fix: altijd de echte serverfout tonen + klasnamen uniek per school (v0.4.1-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- 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
2026-07-18 03:39:04 +02:00
Ramon
4a31dc9351 fix: klas verwijderen toont echte oorzaak i.p.v. kale serverfout (v0.4.0-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 43s
- 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
2026-07-18 03:28:31 +02:00
Ramon
be8ecc5378 fix: gebruikerslijst per school laadde nooit (SELECT DISTINCT/ORDER BY) (v0.3.99-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m13s
- 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)
2026-07-18 03:19:36 +02:00
Ramon
205ba74eff fix: beheerpaneel toont laadfout i.p.v. stilletjes leeg te blijven (v0.3.98-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
- reload() in het beheerpaneel (scholen/klassen/gebruikers) ving elke fout
  stilletjes op en zette alles terug naar lege lijsten, zonder enige melding -
  dat gaf een verwarrend leeg paneel bij een specifieke school terwijl "alle
  scholen" wel werkte, zonder enig aanknopingspunt waarom
- de echte oorzaak verschijnt nu bovenin het paneel (amLoadError), zodat een
  volgende mislukking direct te diagnosticeren is i.p.v. te moeten gokken
2026-07-18 03:08:55 +02:00
Ramon
4ffb69ca61 fix: gebruiker verwijderen toont echte oorzaak i.p.v. kale serverfout (v0.3.97-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m19s
- 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
2026-07-17 22:27:20 +02:00
Ramon
e48a3a4e46 fix: gebruiker verwijderen werkt gegarandeerd - ruimt afhankelijke rijen zelf op (v0.3.96-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 51s
- 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
2026-07-17 21:50:48 +02:00
Ramon
e3036580ce fix: gebruiker verwijderen werkt weer (herstel foreign-key cascade) (v0.3.95-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 32s
- 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
2026-07-17 21:39:29 +02:00
Ramon
30818dc5f2 fix: gebruikers gegroepeerd per school in alle-scholen-weergave + zichtbare verwijderfouten (v0.3.94-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- In de "alle scholen"-weergave stonden gebruikers op rol gegroepeerd, waardoor je niet zag bij welke school ze horen (Ramon/Rubi/leerlingb1b leken los te staan). Ze worden nu per school gegroepeerd (inklapbaar), met systeemmanagers, ouders en losse accounts apart eronder - je ziet meteen wie bij De Ziep hoort
- Bij één gekozen school blijft de indeling op rol/klas; die rolgroepen zijn nu ook inklapbaar
- Verwijderfouten (en andere actie-fouten in het detailpaneel) verschijnen nu in het detailpaneel zelf i.p.v. alleen bovenaan (buiten beeld op mobiel). Als een verwijdering serverzijdig faalt, zie je nu de reden - het lijkt niet langer of "niets gebeurt"
- Verwijderen zelf is end-to-end geverifieerd (DELETE verstuurd, gebruiker verdwijnt); een gesimuleerde 409 toont de melding in het paneel en laat de gebruiker staan
- 7 nieuwe Playwright-checks (school-groepering, zichtbare fout, geslaagde verwijdering) + volledige regressie van 8 suites; 94 servertests groen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 21:21:05 +02:00
Ramon
c86abcf794 fix: onzichtbare knopteksten, lege klassenlijst en correcte klas-schoolregels (v0.3.93-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 32s
- 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
2026-07-17 20:22:36 +02:00
Ramon
aee1c59fbd fix: leerlingen zichtbaar bij gekozen school + uniforme controls in het beheer (v0.3.92-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- 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
2026-07-17 19:46:09 +02:00
Ramon
15618348dc style: alle menu's nagelopen - alles verborgen tot erom gevraagd wordt (v0.3.91-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
- Voortgang: tijdlijn toont 30 regels per keer met een "meer tonen (n)"-knop i.p.v. 200 regels ineens
- Inhoud: de vier blokken (welkomstwoord, melding, widget-uitleg, systeembrede inhoud) zijn inklapbare groepskaarten - alleen het welkomstwoord staat standaard open; open-status overleeft her-renders
- Account: de drie wachtwoordvelden verschijnen pas na een klik op de kop "Wachtwoord"
- Daarmee volgt elke sectie hetzelfde principe: compacte koppen met samenvatting, detail pas op verzoek, verwante items in één kader
- Geverifieerd in headless Chromium (9 nieuwe checks) + volledige regressie van de vier bestaande suites (56 checks)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 14:35:24 +02:00
Ramon
b4eee9f78f feat: gebruikers aan een school koppelen en losmaken (v0.3.90-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 52s
- 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
2026-07-17 14:30:41 +02:00
Ramon
08618ba0b3 feat: Scholen, Klassen en Toewijzingen als groepskaarten (v0.3.89-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 31s
- Scholen: één gezamenlijk kader per school - getinte kop met grijs schoolicoon, naam en de beheerder-teller als uitklaptoggle; beheerders standaard verborgen en als compacte subrijen (36px) met status-glyph (● actief / ○ nog niet geactiveerd, uitleg via title); hernoemen/verwijderen klapt uit binnen hetzelfde kader - geen verwarring meer tussen scholen en beheerders
- Klassen: kaart per klas met leerlingtal in de kop; de reset-/verwijderacties verborgen tot Bewerken
- Toewijzingen: kaart per klas met de huidige stand compact in de kop; bord-/widget-/moduskeuze klapt uit via Bewerken
- Het status-glyph en de kaartkop-wrap zijn onderdeel van het gedeelde groepskaart-component; bewust klein grijs schoolicoon terug (functioneel onderscheid, emoji-scan-uitzondering)
- Geverifieerd in headless Chromium (14 checks) + screenshots 390px/desktop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 14:27:19 +02:00
Ramon
c3492ab2ca feat: Opslag per school met groepskaart-component (v0.3.88-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 55s
- Nieuw herbruikbaar groepskaart-component (amGroupCard): één kader om items die bij elkaar horen - koprij met grijs icoon, titel, samenvatting-als-uitklaptoggle en Bewerken; compacte subrijen; open-status overleeft re-renders (injecteerbare Set zodat ook andere modules hem gebruiken)
- Opslag-tab volledig herbouwd: eerst één school kiezen (of "Systeem (zonder school)" voor systeemmanagers/ouders/losse accounts) i.p.v. alle scholen én alle gebruikers plat met altijd-open editors
- Na keuze één kaart: schoolnaam + quota-samenvatting in de kop (schoolquota bewerken klapt uit), daaronder alleen de gebruikers van díe school als compacte regels; bewerkvelden pas na "Bewerken" per rij; zoeken binnen de school
- "Standaard"-optie benoemt nu de echte rolstandaard ("Standaard (200 MB)") via defaultBytes
- Mobiel: beheer-rijen stapelen niet meer verticaal (kaartkop en subrijen wrappen netjes; subrij ± twee compacte regels i.p.v. >100px)
- Geverifieerd in headless Chromium (13 checks, o.a. dat schoolkeuze en open bewerkrij een her-render overleven - module-state) + screenshots 390px/desktop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 14:20:41 +02:00
Ramon
ebe622588f style: tekstknoppen en consistente secties in het beheer - emoji-sweep afgerond (v0.3.87-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 45s
- Alle resterende emoji uit het beheer: kopieerknoppen zijn tekstknoppen ("kopieer"), wachtwoord-genereerknop heet "genereer", "alles kopiëren" zonder klembord-icoon, schoolrijen zonder 🏫, contextbalk-label is het woord "School", bevestiging is "zeker?" (de rode armed-stijl is het signaal), kopieer-feedback een net vinkje
- Scholen-tabblad: "Bewerken"-tekstknoppen i.p.v. het ✏-potlood; zoek-placeholders zonder vergrootglas-emoji
- CMS-widgetkeuze zonder emoji-prefix in de opties
- Playwright-emoji-scan over alle vijf beheersecties (knoppen/labels/groepen schoon, ▸▾✕✓+ toegestaan) + volledige master-detail-regressie (19 checks) groen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 07:08:17 +02:00
Ramon
af86ff5c47 feat: gebruikersbeheer als master-detail met vast bewerkpaneel (v0.3.86-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
- Gebruikers staan in een echte tabel (initiaal-avatar + naam | functie | klas | status) met kolomkoppen en uitgelijnde kolommen; klikken op een rij selecteert (accent-markering) zonder her-render, dus het zoekveld houdt focus
- Rechts een vast detailpaneel: bewerken (functie, klaskoppeling, klas, wachtwoord, acties), aanmeldverzoeken afhandelen én nieuwe gebruikers aanmaken gebeuren allemaal dáár - één plek, één patroon
- Eenmalige wachtwoorden/codes verschijnen als badge in het paneel; na aanmaken is de nieuwe gebruiker direct geselecteerd; selectie overleeft herladen en wordt gevalideerd tegen de data
- Aanmeldverzoeken zijn gewone tabelrijen met een "nieuw"-chip; de losse toevoeg-formulieren per klas zijn vervangen door één "Nieuwe gebruiker"-knop in de werkbalk (met rolkeuze)
- Smal scherm: het detailpaneel wordt een overlay met sluitknop (onder de tabel stapelen is onbruikbaar bij grote scholen)
- Menu-items herzien: de sectie heet "Scholen" (geen "Systeem > Systeem"-verdubbeling meer) en de Systeem-groep is geordend als Scholen / Inhoud / Opslag
- Eerste emoji-sweep in de omgebouwde delen (actieknoppen zijn tekstknoppen); zoekfilter en inklapbare klasgroepen werken ongewijzigd
- Geverifieerd in headless Chromium (19 checks, incl. focusbehoud, overlay-gedrag en selectie-na-reload)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-17 07:06:18 +02:00
Ramon
8b4bd47407 style: zakelijk ontwerpsysteem voor Instellingen (v0.3.85-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
- Volledig gescoped op #settingsModal zodat het bord speels blijft: systeemfont i.p.v. Quicksand, controls 32px (40px op aanraakschermen), vlakke 1px-randen i.p.v. schaduwen en pastel-pills
- Rijen worden tabelregels met scheidingslijnen en hover; groepskoppen zijn subtiele sticky sectierijen; navigatie-actief is neutraal grijs met accent-inzetlijn; iconen gedempt in grijstinten
- Knoppen één systeem: subtiel omrand, danger als rode tekst die pas gevuld rood wordt na wapenen; chips/resultaten/segmentknoppen omrand vlak
- Hoog contrast expliciet teruggebracht binnen de nieuwe scope; sectiekop-icoon in eigen span zodat CSS hem kan dempen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 20:42:53 +02:00
Ramon
526e4e5d52 feat: CMS-sectie Inhoud voor de systeemmanager (v0.3.84-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
- Nieuwe sectie "Inhoud" in Instellingen (groep Systeem, alleen systeemmanager, doorzoekbaar via het sectieregister)
- Welkomstwoord (NL/EN) verschijnt op het inlogscherm, direct aanpasbaar zonder release
- Melding voor alle gebruikers: aan/uit, niveau informatie/belangrijk, per taal; verschijnt als wegklikbare banner onder de menubalk en elke wijziging krijgt een nieuw id zodat een eerder weggeklikte melding opnieuw verschijnt
- Widget-uitleg: per widget een eigen beschrijving die de standaardtekst in de widgetbibliotheek vervangt (leeg = standaard, placeholder toont de standaardtekst)
- Snelkoppelingen naar de bestaande kanalen voor systeembrede inhoud: gedeelde bibliotheek (borden & ankerdiagrammen, map 🌐 Systeem) en de afbeeldingencatalogus
- settings.js kreeg een generiek haakje (showSettingsDom) zodat modules met een eigen paneel zich in het sectieregister kunnen voegen
- Geverifieerd in headless Chromium (15 checks): banner + dismiss + nieuw-id-gedrag, welkomstwoord, bewerken/opslaan, galerij-override, sectie onzichtbaar voor andere rollen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 19:33:49 +02:00
Ramon
aa4c5faaf9 feat: server-kant CMS voor de systeemmanager (v0.3.83-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 31s
- 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
2026-07-16 19:29:08 +02:00
Ramon
2fff8a3aa5 perf: gzip-vangnet in nginx voor geproxyde responses (v0.3.82-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m4s
- gzip on + gzip_proxied any (zonder die laatste comprimeert nginx geproxyde responses helemaal niet) + gzip_vary + min_length 1024 voor css/js/json/svg/tekst
- Geen dubbele compressie: responses die al Content-Encoding dragen (brotli/gzip uit de app) laat nginx ongemoeid; dit vangt alleen wat de app mist
- Cache-beleid blijft bewust op één plek (de app), zodat lokaal en productie identiek gedragen
- Configtest erbij in security-config-stijl

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 19:02:53 +02:00
Ramon
456d664718 perf: compressie, cache-headers, cache-busting en parallel ladende scripts (v0.3.81-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m14s
- @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
2026-07-16 19:01:25 +02:00
Ramon
cb97dc2bb1 feat: breid taalwidgets en plaatjesbibliotheek uit (v0.3.80-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 31s
2026-07-16 16:27:38 +02:00
Ramon
4356658c96 fix: versietest pint geen vast nummer meer (v0.3.79-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 43s
- De CI faalde omdat een test het versienummer hardcodeerde (0.3.77-beta) en dus bij elke reguliere versiebump breekt
- De test bewaakt nu het echte beleid: de dev-branch voert altijd een -beta-versie en de VERSION-file loopt gelijk met de const in core.js (die controle stond er al en blijft)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 15:01:59 +02:00
Ramon
2a0de85152 fix: vastgezette lat kleeft altijd en vinger-jitter breekt de snap niet meer (v0.3.78-beta)
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 32s
- Op een aanraakscherm wiebelt een vinger in de eerste millimeters: de richtingstoets wees de snap daardoor af en de haal bleef de vinger volgen
- Vastgezet gereedschap (🔒) kleeft nu onvoorwaardelijk binnen 28px van de rand - geen richtingstoets; het slot betekent "hier teken ik langs"
- Losse lat: richting wordt over een langere basis gemeten (10px i.p.v. 7) met een ruimere hoek (40° i.p.v. 30°), zodat vinger-jitter de evenwijdige intentie niet meer maskeert
- Inktlaag kreeg touch-action:none zodat de browser vingerhalen nooit als scroll-gebaar afvangt
- Geverifieerd met échte touch-events (CDP) inclusief nagebootste vinger-jitter: 7 nieuwe checks (vastgezet kleeft mét jitter, op de lat tekenen zonder verschuiven, los evenwijdig kleeft, wegdraaiend blijft vrij) + de vier bestaande muis-suites (42 checks) blijven groen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 14:45:51 +02:00
Ramon
7a5d7c57ff feat: maak widgets speelser en kindvriendelijker (v0.3.77-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 41s
2026-07-16 14:15:28 +02:00
Ramon
85268050fd feat: slot op liniaal/geodriehoek en richtingsbewust langs de lat tekenen (v0.3.76-beta)
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 34s
- Slot-knop (🔓/🔒) op liniaal én geodriehoek: vastgezet gereedschap kan niet meer verschuiven of draaien (draaiknop verdwijnt), er kan alleen nog langs getekend worden; tweede klik ontgrendelt
- Naast de lat beslist nu de richting i.p.v. één vaste afstand: binnen 22px van een rand wacht de haal de eerste ~7px beweging af - loopt die grofweg evenwijdig aan de rand (≤30°), dan kleeft hij, anders blijft hij vrij; een tik zet gewoon een stip
- Daarmee is langs de lat tekenen betrouwbaar (ook met dikke digibord-vingers) zónder dat de lat vrije halen in de buurt grijpt
- Op de lat zelf blijft de intentie ondubbelzinnig: direct kleven binnen 24px van de rand
- Geverifieerd in headless Chromium: 12 nieuwe checks (richtingsregel, stip-tik, slot aan/uit, vastgezet = onverplaatsbaar maar betekenbaar) + de drie bestaande suites (35 checks) blijven groen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 14:04:15 +02:00
Ramon
5d12d2f8ec feat: kleinere snapzone, dunnere pennen, potlood met karakter en kalligrafische vulpen (v0.3.75-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 40s
- Snapzone naast de liniaal/geodriehoek verkleind van 24 naar 10px: vrije halen in de buurt worden niet meer "gegrepen"; op de lat zelf blijft de ruime 24px-zone gelden
- Diktekiezer uitgebreid met dun (1 en 2, naast 3/5/8)
- ✏️ heet nu Potlood en tekent zo: een zachte, gelijkmatige lijn (~85% dekking) via de overlay-laag, dus égaal ook waar de haal zichzelf kruist
- Vulpen schrijft nu echt kalligrafisch met een 45°-penpunt: dik in de neerhaal, dun in de ophaal (gemeten 11px vs 1,5px bij dikte 5); stylusdruk versterkt het effect
- Geverifieerd in headless Chromium (9 checks, pixelmetingen van dekking, snapzones en lijndiktes)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 13:55:36 +02:00
Ramon
6e7f24eb40 fix: pen op de lat tekent nu langs de rand i.p.v. de lat te verslepen (v0.3.74-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
- In de praktijk zet je de pen óp de liniaal-/geodriehoekrand, zoals bij een echte lat - maar dat versleepte het gereedschap, waardoor "langs de lat tekenen" niets leek te doen
- Landt de pen (pen/vulpen/stift, penmodus actief) binnen 24px van een tekenrand - ook óp de lat zelf - dan start een vastgeklikte haal en blijft het gereedschap liggen; de events lopen via pointer capture gewoon door naar de inktlaag
- Verder van de rand blijft het gereedschap gewoon te verslepen, en de draaiknop houdt voorrang
- Startzone naast de lat verruimd van 16 naar 24px (digibord-vingers)
- Geverifieerd in headless Chromium: 7 nieuwe checks (tekenen op de randzone zonder de lat te verschuiven, verslepen vanuit het midden, draaien in penmodus, idem geodriehoek) + de bestaande 14 snap-checks blijven groen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 13:46:18 +02:00
Ramon
abafcafe3a feat: breid natuurwidgets uit
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 30s
2026-07-16 13:43:46 +02:00
Ramon
55520ba367 feat: tekenen langs liniaal en geodriehoek + geodriehoek met twee niveaus (v0.3.72-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
- Langs de lat tekenen: begint een haal binnen 16px van een rand van de liniaal of geodriehoek, dan kleeft de hele haal aan die rand - kaarsrecht, ook gedraaid (randen draaien mee); bewust wegtrekken (>60px) laat weer los; gum en vrije halen blijven onaangetast
- Gereedschappen liggen nu boven de inktlaag, zoals een echte lat op het bord: ook in penmodus te verplaatsen, draaien en instellen
- Geodriehoek herbouwd met twee niveaus: basis (grote hoofdhoeken 30/45/60/…, hele centimeters - voor jonge leerlingen) en volledig (graden per 2° met dubbele schaal links- én rechtsom, millimeters, evenwijdige hulplijnen per cm - voor meetkunde in de bovenbouw en daarna)
- Geodriehoek: cm-schaal vanaf 0 in het midden zoals echt, maat in hele cm op de knop (6/8/12 cm), 90°-markering, begrensd op de bordbreedte; alleen de driehoek zelf vangt aanrakingen zodat er vlak naast de schuine randen getekend kan worden
- Geverifieerd in headless Chromium (14 checks, o.a. pixelmetingen van rechtgetrokken halen op rechte én 30°-gedraaide randen)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 13:38:14 +02:00
Ramon
93c287979c feat: breid wereldorientatie-oefeningen uit (v0.3.71-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 30s
2026-07-16 11:44:30 +02:00
Ramon
100dc62087 feat: verbeter wereldorientatie met echte beelden (v0.3.70-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
2026-07-16 10:31:50 +02:00
Ramon
88d1f9cb86 feat: voeg wereldorientatiewidgets toe (v0.3.69-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 26s
2026-07-16 09:42:45 +02:00
Ramon
22233c5f92 fix: laat de minutenwijzer de urenwijzer verzetten (v0.3.68-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 24s
2026-07-16 09:14:15 +02:00
Ramon
66fbac7221 fix: laat de oefenklok door alle 24 uren draaien (v0.3.67-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
2026-07-16 08:53:09 +02:00
Ramon
ecf60304a9 fix: centreer widgetopdrachten en herstel 24-uursinvoer (v0.3.66-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 39s
2026-07-16 08:41:26 +02:00
Ramon
652e039a39 feat: vloeiende pen en stift, egale markeerstift en logischere liniaal (v0.3.65-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 31s
- Pen, vulpen en stift tekenen nu met kwadratische curves door de middelpunten van de pointer-stappen: geen hoekige knikken meer bij snelle halen (het "blikkerige" beeld)
- Markeerstift tekent tijdens de haal dekkend op een eigen doorschijnende laag en wordt bij het loslaten in één keer met vaste transparantie op de inktlaag gezet: overlappingen binnen één haal geven geen donkere vlekken meer, een tweede haal eroverheen wordt wél donkerder (zoals een echte stift)
- devicePixelRatio wordt bij elke resize vers gelezen (scherp blijven na verplaatsen naar een ander scherm of zoomen)
- Liniaal: logische lengtes in hele centimeters (5/10/15/20 cm, knop toont de actuele maat), schaal telt vanaf 0 met een kantlijn zoals een echte liniaal, lengte begrensd op de bordbreedte, en de lengte staat op de liniaal zelf
- Liniaal en geodriehoek: draaien toont live de hoek in graden en klikt vast op veelvouden van 45° (badge kleurt groen); knoppen hebben leesbare titels (Lengte / Hoog contrast)
- Geverifieerd in headless Chromium (12 checks, o.a. pixelmeting van de markeerstift-kruising en exacte 45°-snap)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 08:37:23 +02:00
Ramon
3e6ab09e1f fix: ankerkiezer opent alleen op een bewuste tik en tekenbalk krimpt weer geminimaliseerd (v0.3.64-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 31s
- Oorzaak van het "vanzelf" openende uploadvenster gevonden: elk leeg plaatjesvak in een ankerdiagram had eigen pointer/click-handlers die het pannen blokkeerden en de plaatjeskiezer (met uploadtab) openden bij elke veeg die erop begon - op een telefoon was dat vrijwel elke aanraking, dus ook direct na het verversen
- Lege plaatjesvakken lopen nu via dezelfde tik/sleep-logica als de cellen (6px-drempel): vegen pant het diagram, alleen een stilstaande tik opent de kiezer; woord bewerken blijft ongewijzigd
- Tekenbalk op telefoons weer gecentreerd op inhoudsbreedte i.p.v. vastgepind aan beide randen: geminimaliseerd is hij weer smal (121px i.p.v. volle breedte), nog steeds in de eigen laag boven +/borden
- Hardening: een onbekende achtergrond-waarde in opgeslagen borddata crashte de hydrate en logde de gebruiker stilletjes uit bij verversen - valt nu terug op de standaardachtergrond
- Geverifieerd in iPhone-emulatie (8 checks): geen venster na verversen, veeg pant, tik kiest, tekenbalk smal en vrij van +

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 08:09:33 +02:00
Ramon
99e807595a feat: alle widgets mobielvriendelijk maken (v0.3.64-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 51s
2026-07-16 08:02:20 +02:00
Ramon
7bd35685fe feat: sluitknop op geminimaliseerde widgets in de dock (v0.3.63-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 45s
- Elke dock-pil (geminimaliseerde widget) heeft nu een eigen ✕ om de widget direct te sluiten, zonder hem eerst terug op het bord te zetten
- Klik op de pil zelf blijft de widget herstellen; de ✕ deelt hetzelfde sluitpad als de ✕ in de widgetkop
- Groter tikvlak voor de ✕ op aanraakschermen; titels wisselen mee met de taal
- Geverifieerd in headless Chromium (8 checks, incl. serialisatie na sluiten/herstellen)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 07:50:41 +02:00
Ramon
87b22efc56 fix: tekstzoom terug op telefoon en tekenbalk botst niet meer met +/borden (v0.3.62-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 28s
- A−/A+ (tekstzoom, toegankelijkheid) staan weer bovenaan in de telefoon-balk; alleen het percentage, de catalogusknop en volledig scherm blijven daar verborgen (opslaan-knop zonder minimumbreedte zodat alles past, ook op 320px)
- Echte oorzaak van het steeds opende afbeeldingen-/bordenscherm gevonden: op telefoons lagen de +-knop en de paarse bordenknop bovenop de tekenbalk - wissen/sluiten aantikken opende dus het +-menu of het bordenpaneel
- De tekenbalk ligt op telefoons nu in een eigen laag bóven de knoppenrij (groeit omhoog, dus veilig bij elke schermbreedte); + en borden houden de onderrand
- Geverifieerd op 390px én 320px (8 checks): geen overlap, sluit-/wisknop raakt echt de tekenbalk, balk past volledig, A−/A+ zichtbaar

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 07:44:38 +02:00
Ramon
8b76cf3b4b fix: menubalk past op telefoon - afbeeldingenscherm opent niet meer per ongeluk (v0.3.61-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 26s
- Op telefoons (≤600px) liep de menubalk 143px over: 💾 Opslaan en ⚙ Instellingen vielen buiten beeld en 🖼 (afbeeldingscatalogus) zat precies onder de duim, waardoor het schermvullende afbeeldingen/upload-scherm steeds per ongeluk opende
- Telefoon-balk toont nu alleen de essentie (🔊, 📁, 💾, ⚙); zoom en de catalogusknop verdwijnen daar - afbeeldingen blijven bereikbaar via het +-menu
- Afbeeldingscatalogus: titelbalk met sluitknop blijft vastgeplakt in beeld tijdens scrollen, en op mobiel 100dvh i.p.v. 100vh zodat er niets achter de browserbalk wegvalt
- Geverifieerd in iPhone-emulatie (5 checks): balk past volledig, opslaan/instellingen bereikbaar, sluitknop blijft zichtbaar, catalogus via +-menu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 07:25:58 +02:00
Ramon
bdb5ef9875 feat: instellingen volledig herbouwd - sectieregister, rolweergaven en zoeken (v0.3.60-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
- 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
2026-07-16 07:07:53 +02:00
Ramon
42fc323ab1 fix: toon gekoppelde ouders in de gebruikerslijst van de school (v0.3.59-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 28s
- 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
2026-07-16 00:23:51 +02:00
Ramon
5f9ffbfdde feat: client-kant ouderportaal (v0.3.58-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 37s
- Derde tab "Ouders" in het inlogscherm: ouderaccount registreren (naam + 2× wachtwoord) met directe login
- Nieuw ouderportaal (parent.js): kinderenlijst met school/klas of "thuis", kind toevoegen (wachtwoord eenmalig als badge bij de rij), koppelen via ouder-code, aanmelden bij een school via doorzoekbare lijst met aanmeldstatus op de rij, voortgang per kind uitklapbaar, ontkoppelen met bevestiging
- Beheer: blok "Aanmeldingen (n)" bovenaan de Gebruikers-tab met per verzoek adopteren (klas + optionele andere gebruikersnaam bij naamconflict), koppelen aan een bestaande leerling, of afwijzen
- Beheer: actie "👪 Ouder-code" op elke leerlingrij (eenmalige code als badge); groep "Ouders" in de gebruikerslijst
- Foutmeldingen in het inlogscherm nu zichtbaar op alle drie de tabs
- i18n nl/en; volledige ouder- en schoolflow geverifieerd in headless Chromium (21 checks)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
2026-07-16 00:14:05 +02:00
Ramon
9d83e13bc3 feat: server-kant ouderportaal (v0.3.57-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- 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
2026-07-16 00:02:16 +02:00
Ramon
a9bf2270d0 fix: beperk rollen en klaskoppeling tot staf (v0.3.56-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
- 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
2026-07-15 23:56:39 +02:00
Ramon
0d644ba41d feat: voeg afbeeldingscatalogus en opslagquota toe
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
2026-07-15 23:45:17 +02:00
Ramon
3ad7c80bad feat: koppel geldniveaus aan realistische winkelprijzen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 24s
2026-07-15 22:51:49 +02:00
Ramon
54a95b2c19 revert: herstel officiële biljetten 2026-07-15 22:36:42 +02:00
Ramon
ff91ae0f2b test: gebruik eigen oefenbiljetten zonder markering
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
2026-07-15 22:32:20 +02:00
Ramon
2742196c11 feat: toon echt euromateriaal in geldwidget
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 47s
2026-07-15 22:19:15 +02:00
Ramon
2fa9879107 feat: maak geld realistischer en uitsleepbaar
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 23s
2026-07-15 21:59:38 +02:00
Ramon
c2e6622546 feat: breid geldrekenen uit met niveaus en spellen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
2026-07-15 21:45:40 +02:00
Ramon
2d176cfc30 feat: voeg 24-uursoptie aan oefenklok toe
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 30s
2026-07-15 21:31:57 +02:00
Ramon
d900c25339 feat: breid niveau drie en wijzerbediening uit
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 23s
2026-07-15 21:10:31 +02:00
Ramon
e0220cdd00 fix: toon klokniveaus en verplaats koppeling
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 32s
2026-07-15 21:03:59 +02:00
Ramon
6e920e32db feat: koppel klokwijzers en start direct
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 20s
2026-07-15 20:53:47 +02:00
Ramon
cd15c0435b feat: voeg klokniveaus en typmodus toe
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
2026-07-15 20:47:22 +02:00
Ramon
6e17dc5982 fix: voeg vrije uurmarkeringen toe
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
2026-07-15 20:38:30 +02:00
Ramon
54782781bf fix: voorkom overlap op klokwijzerplaat
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 50s
2026-07-15 20:35:49 +02:00
Ramon
4cc8d466b8 feat: voeg sleepbare tijdblokken toe
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
2026-07-15 20:32:12 +02:00
Ramon
72ea486518 feat: verbeter klokopdrachten en bediening
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 44s
2026-07-15 20:23:21 +02:00
Ramon
d02f7de72d feat: maak oefenklok visueel bedienbaar
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 20s
2026-07-15 20:12:43 +02:00
Ramon
3efe57b43c feat: breid oefenklok uit met niveaus en spellen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
2026-07-15 20:05:50 +02:00
Ramon
7287e49e0c v0.3.38-beta: koppel ook schoolbeheerders en systeemmanagers aan klassen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 23s
- 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
2026-07-15 16:35:55 +02:00
Ramon
804ffb98f6 v0.3.37-beta: één functie per gebruiker + klas-koppeling voor groepsleiding
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 35s
- 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)
2026-07-15 16:20:21 +02:00
Ramon
df921d5968 v0.3.36-beta: scholen mét schoolbeheerders in het Systeem-tabblad
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
- 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)
2026-07-15 16:02:57 +02:00
Ramon
b2ca4f93d5 v0.3.35-beta: schollenlijst in Systeem + verborgen formulieren + gelijke maten
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 32s
- Systeem-tabblad toont nu een overzicht van alle scholen als volwaardige
  rijen met een bevestigde verwijder-actie
- Het school-toevoegen-formulier zit achter een "+ School"-knop (zelfde
  patroon als gebruikers) en houdt de naam-bevestiging; de uitlegtekst is
  weg - teksten kort en krachtig
- Alle formulieren en knoppen in het beheer op dezelfde controlhoogte;
  fix: de rij-flexregels van invoervelden drukten velden binnen een
  veldlabel (kolom-flex) verticaal plat
- Ontbrekende veldlabel-tekst "Naam" toegevoegd (nl/en)
2026-07-15 15:53:10 +02:00
Ramon
03b8e6a41b v0.3.34-beta: één interactiesysteem in het beheer + veilige acties
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 22s
- Elk invoerveld/select/knopgroep in het beheer heeft nu een zichtbaar
  mini-label (field-helper): Functie, Extra functies, Klas, Wachtwoord,
  Naam, Acties - een placeholder alleen is geen toelichting meer
- Acties met gevolgen (nieuw wachtwoord, nieuwe koppelcode, verwijderen,
  wachtwoord-resets) zijn een aparte knopklasse: rood-zacht, tooltip die
  het gevolg uitlegt, en een tweede klik als bevestiging (arm-then-confirm,
  wapening loopt na 4s af); alle native confirm()-dialogen zijn vervangen
- Nieuw gegenereerde wachtwoorden/koppelcodes verschijnen direct bij de
  gebruiker in de rij (badge met kopieerknop, rij klapt vanzelf open),
  niet meer bovenaan; de topbalk is alleen nog voor fouten
- Klassen zijn volwaardige rijen op dezelfde maat als de gebruikersvakken,
  met leerlingaantal en de reset/verwijder-acties als bevestigde knoppen
- Systeem-tabblad: schoolkiezer-balk daar verborgen (school-onafhankelijk);
  school aanmaken heeft nu een uitleg, een naam-label, een knop die pas
  actief wordt bij een echte naam en een bevestigingsstap met de naam erin
- Toevoegen van gebruikers/klassen: knop pas actief bij geldige naam
2026-07-15 15:37:05 +02:00
Ramon
75244cde14 v0.3.33-beta: overzichtelijker beheerpaneel zonder tab-afhankelijkheid
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
- Vaste schoolkiezer-balk (🏫) boven alle beheer-tabs voor de systeem-
  manager: geen omweg meer via het Systeem-tabblad om een school te kiezen;
  de kiezer is uit het Systeem-tabblad verdwenen en de hint verwijst naar
  de balk erboven
- Gebruikers-tab: leerlingen staan onder inklapbare groepskoppen per klas
  (standaard dicht, met aantal en ▸/▾-pijl); zoeken kijkt altijd door alle
  groepen heen zodat een treffer nooit verstopt zit
- Gebruikersrijen tonen standaard alleen naam en rol/klas; alle knoppen en
  velden (rol wijzigen, extra functies, klas, wachtwoord, koppelcode,
  verwijderen) zitten achter een ✏-knopje per rij; uitgeklapte staat blijft
  behouden over her-renders
2026-07-15 05:03:30 +02:00
Ramon
54eb8b18cb v0.3.32-beta: één tabbalk in Instellingen (geen geneste beheer-tabs meer)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
- De beheer-tabs (Systeem/Klassen/Gebruikers/Toewijzingen/Voortgang) staan
  nu als losse knoppen direct in de instellingen-tabbalk naast Account en
  Weergave, in plaats van een tweede tabbalk binnen het Beheer-tabblad
- admin.js vult #adminTabsSlot en herbouwt bij gebruikers-/taalwissel;
  data wordt geladen bij binnenkomst in het beheer-deel, wisselen tussen
  beheer-tabs onderling hergebruikt de geladen data
- De losse "Beheer"-knop en de geneste .ed-tabs-balk zijn verwijderd;
  de tabbalk loopt om op smalle schermen (flex-wrap)
2026-07-15 04:37:15 +02:00
Ramon
d5e0b361bf v0.3.31-beta: geef ankerdiagrammen een prominentere plek
- Ankerwidget staat nu vooraan in de widgetbibliotheek (eerste kaart)
- Nieuw -item in het +-menu: plaatst direct een ankerdiagram op het bord
- Duidelijkere uitleg wat een ankerdiagram is (woordweb voor
  woordenschatonderwijs: woorden in samenhang rond een themawoord leren),
  in de bibliotheekkaart, de widget-hint en het +-menu-label (nl/en)
2026-07-15 04:32:25 +02:00
Ramon
6d5d4ef520 v0.3.30-beta: ankerdiagrammen als vaste tak in de hoofdverkenner
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 37s
- Nieuwe generieke withBranch-combinator (explorer.js): een tweede adapter
  verschijnt als vaste map in de root van de hoofdboom; item-sleutels worden
  geprefixt en slepen tussen de takken wordt geweigerd (ander soort inhoud)
- De borden-verkenner (📁 in de menubalk) toont nu " Ankerdiagrammen" met
  daarin de eigen diagrammappen én de gedeelde 🌐/🏫-takken voor ankers -
  borden en ankerwoorden op één plek, met dezelfde regels (slepen, kopiëren,
  alleen-lezen, één-klik-mappen)
- Diagram openen vanuit de hoofdverkenner plaatst een nieuw ankerwidget op
  het huidige bord met dat diagram geladen; het 🗂-knopje op een widget
  blijft in dát widget laden (adapter naar module-niveau getild met een
  onOpen-parameter, gedeeld door beide ingangen)
2026-07-15 04:12:43 +02:00
Ramon
a919e31e4c v0.3.29-beta: alleen-lezen in de verkenner + mappen openen met één klik
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
- Gedeelde items die je niet mag beheren tonen een 🔒 en weigeren hernoemen/
  verplaatsen/verwijderen direct in de client met een duidelijke melding
  (kopiëren blijft altijd werken); mappen met vergrendelde inhoud tonen 🔒
- Nieuwe knop "🔒 Alleen lezen" in de school-tak, alleen zichtbaar voor
  schoolbeheerder/systeemmanager: vergrendelt het geselecteerde item of
  (zonder selectie) de hele huidige map, en ontgrendelt bij tweede keer
- Mappen openen nu met één klik (Windows-webstijl); slepen blijft werken
  via de bewegingsdrempel en de klik direct ná een sleep wordt genegeerd;
  hernoemen/verwijderen zonder selectie werkt op de huidige map (via het
  broodkruimelpad)
2026-07-14 22:53:47 +02:00
Ramon
24180beec1 v0.3.28-beta: alleen-lezen-vlag voor gedeelde items (server)
- 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
2026-07-14 22:49:17 +02:00
Ramon
e21bc177e1 v0.3.27-beta: gedeelde bibliotheek in de mappenverkenner
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
- withSharedBranches (explorer.js): wikkelt een eigen-inhoud-adapter en
  voegt in de root de vaste takken "🌐 Systeem" (systeembreed) en
  "🏫 School" (binnen de eigen school) toe
- Slepen tussen bomen = kopiëren: eigen bord/diagram of hele map naar een
  gedeelde tak publiceert (POST /shared, kopie - origineel blijft); gedeeld
  item of map naar de eigen boom kopieert (borden krijgen nieuw id + verse
  wids); binnen een tak is slepen echt verplaatsen (PATCH, server bewaakt)
- Dubbelklik op een gedeeld bord kopieert en opent het; op een gedeeld
  ankerdiagram laadt het in het widget
- Verkenner ondersteunt nu async adapters (init/mutaties), groene
  ok-meldingen ("Gedeeld ✓"/"Gekopieerd ✓") en een eigenaar-hint per tegel
- Gasten en leerlingen zien alleen de eigen boom
2026-07-14 22:34:54 +02:00
Ramon
939c4ee68b v0.3.26-beta: server-API voor de gedeelde bibliotheek
- 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
2026-07-14 22:27:16 +02:00
Ramon
6de569c892 v0.3.25-beta: hernoem rol naar systeemmanager
- 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
2026-07-14 22:24:34 +02:00
Ramon
86478ebaca v0.3.24-beta: bewaar ankerdiagrammen los van het bord (bibliotheek)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s
- 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)
2026-07-14 22:14:37 +02:00
Ramon
e327049f1b v0.3.23-beta: beheer woordthema's in dezelfde mappenverkenner
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 33s
- 🗂-knop in de woorden-editor opent de Windows-stijl verkenner met de
  thema's van de actieve taal (★-tegels met woordaantal); dubbelklik
  selecteert het thema in de editor
- Geneste mappen via het folder-veld als pad; nieuwe lege mappen bestaan
  sessie-lokaal tot er een thema in wordt gezet (mappen zijn afgeleid van
  de themavelden)
- Ankerthema's staan vast in de map "": niet versleepbaar, hernoemen/
  verwijderen geweigerd (anchor.js beheert ze)
- Het 📁-knopje + inline map-invoer uit v0.3.21 zijn vervangen door de
  verkenner; de optgroup-weergave in de dropdowns blijft en toont het pad
2026-07-14 22:01:59 +02:00
Ramon
1151c71049 v0.3.22-beta: Windows-stijl mappenverkenner voor borden
- Nieuw public/js/explorer.js: generieke verkenner-modal met broodkruimelpad,
  werkbalk (nieuwe map/nieuw item/hernoem/verwijder met bevestigingsstap),
  raster van map- en itemtegels, dubbelklik om te openen en slepen om te
  verplaatsen (pointer-events, werkt op touch; drop-doel licht op)
- Geneste mappen zonder datamigratie: nesting zit in de mapnaam als pad met
  "/"-scheiding; server en toewijzingen (bord-id's) blijven onaangeroerd;
  "/" wordt geweigerd in nieuwe namen
- Borden-adapter in board.js: bordtegels met boardThumb-preview, actief bord
  gemarkeerd en gevolgd bij verplaatsen/hernoemen (prefix-rewrite), map
  verwijderen alleen als leeg, laatste bord onverwijderbaar
- 📁-knop in de menubalk opent de verkenner; het oude mappenpaneel
  (renderBoardsPanel + #boardsPanel + CSS) is vervangen; de boardStrip-
  snelwissel blijft; curSlot() heelt lege actieve mappen zelf
2026-07-14 21:58:24 +02:00
Ramon
f2243c13c4 v0.3.21-beta: groepeer woordthema's in mappen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s
- Thema's krijgen een optioneel folder-veld; de theme-dropdown in de
  woorden-editor en de kaart-dropdowns in de widgets (fillWordSourceSel)
  groeperen per map met optgroups, mapl0ze thema's bovenaan
- Nieuw 📁-knopje in de editor zet het gekozen thema in een map (leeg veld
  haalt het eruit); zichtbaarheid volgt de themakeuze
- Ankerthema's komen automatisch in de vaste map "" (id-koppeling met de
  ankerwidget blijft onaangeroerd)
- Nieuwe i18n-teksten themeFolder/themeFolderPh (nl/en)
2026-07-14 16:35:22 +02:00
Ramon
18775ae0ec v0.3.20-beta: hernoem mappen en verplaats borden tussen mappen
- Bordenpaneel: dubbelklik op een mapnaam hernoemt de map (zelfde
  inline-input-patroon als bord hernoemen)
- Nieuw 📁-knopje per bord toont een inline select met de overige mappen;
  bij keuze verhuist het bord, het actieve bord volgt mee (BS.f/BS.b
  worden gecorrigeerd) en een leeggeraakte map krijgt een vers bord
- renameTip vermeldt nu ook mappen; nieuwe i18n-tekst moveBoardTip (nl/en)
2026-07-14 16:29:25 +02:00
Ramon
2ce82ab998 v0.3.19-beta: verstuur bij het opslaan alleen gewijzigde secties
- 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)
2026-07-14 16:26:16 +02:00
Ramon
6980d72041 v0.3.18-beta: bewaar en herstel eerdere versies van borddata
- 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)
2026-07-14 16:23:10 +02:00
Ramon
b28d25296d v0.3.17-beta: detecteer save-conflicten tussen tabbladen
- 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)
2026-07-14 16:19:25 +02:00
Ramon
d0ab0b2483 v0.3.16-beta: deel één widget via de Toewijzingen-tab
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
- assignmentRow: na bordkeuze verschijnt een widget-select ("heel bord" of
  één widget van dat bord, client-side uit BS-borddata); bij een widgetkeuze
  een modus-select (kijken/werken); PUT-body krijgt widgetId + mode mee
- Widgets zonder wid (borden van vóór v0.3.13) worden overgeslagen tot de
  eerstvolgende save van dat bord
- Infotekst toont nu ook widget en modus van de huidige toewijzing
- Nieuwe i18n-teksten amWholeBoard/amModeWatch/amModeWork (nl/en)
2026-07-14 15:53:31 +02:00
Ramon
873f8e0bee v0.3.15-beta: kijken/werken-modus in de leerling-omgeving
- pupil.js: verwerkt de modus uit /my/assignment; "kijken" toont een
  meekijk-badge, blokkeert alle interactie en logt geen voortgang; "werken"
  (default) is het bestaande gedrag met voortgang-logging
- Poll-interval dynamisch: 5s in kijken-modus (wijzigingen van de leerkracht
  komen vlot door), 20s in werken-modus; moduswissel her-rendert direct
- teach.css: generieke kijken-overlay (pointer-events:none op de widget-body),
  werkt voor elk widgettype zonder eigen readonly-ondersteuning
- Nieuwe i18n-tekst pupilWatching (nl/en)
2026-07-14 15:51:20 +02:00
Ramon
b13fe65da5 v0.3.14-beta: sta toewijzing van één widget toe met kijken/werken-modus (API)
- 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
2026-07-14 15:48:14 +02:00
Ramon
d27419b9f6 v0.3.13-beta: geef widgets een stabiel instantie-id (wid)
- 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).
2026-07-14 15:46:15 +02:00
Ramon
be89a7b498 v0.3.12-beta: herstel sessie na refresh (regressie beveiligingsaudit)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
De beveiligingsaudit (v0.3.03-beta) verving de client-side Bearer-token
(localStorage) door een HttpOnly session-cookie, maar de sessie-herstel-IIFE
in app.js las nog steeds de verwijderde TOKEN-variabele uit - een
ReferenceError die de hele boot-restore liet crashen. Resultaat: elke
paginaverversing verloor de ingelogde staat, ook al bleef de cookie zelf
geldig. Fix: geen client-side tokencheck meer nodig (de HttpOnly-cookie is
sowieso niet leesbaar door JS en gaat vanzelf mee); /auth/me wordt nu altijd
geprobeerd en valt bij een 401 terug op gastmodus.
2026-07-14 15:31:17 +02:00
Ramon
aac7fd1c68 v0.3.11-beta: toon leerlingvoortgang in nieuw admin-tabblad
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
- Nieuwe "Voortgang"-tab naast Toewijzingen: leerkracht kiest een leerling
  (zelfde per-klas gegroepeerde select) en ziet de volledige tijdlijn van
  GET /progress/pupil/:id (widgettype, score, tijdstip)
- fillPupilSelect(): gedeelde helper voor de per-klas leerlingselectie,
  onttrokken uit Toewijzingen zodat Voortgang hem ook gebruikt
- Nieuwe i18n-teksten amProgress/amProgressEmpty (nl/en)
2026-07-14 15:05:16 +02:00
Ramon
04e9bf3847 v0.3.10-beta: rapporteer widgetvoortgang vanuit leerling-speelmodus
- board.js: fix bug waarbij de opts-parameter (readonly e.d.) nooit bij
  mountMath aankwam, doordat de registry-wrapper hem liet vallen
- math.js/letters.js/ball.js: nieuwe opts.onProgress-callback, aangeroepen
  bij resp. einde rekenronde, voltooid woord en geknapte ballon
- pupil.js: geeft onProgress mee bij het mounten en post het naar
  /my/progress; alleen actief in leerling-speelmodus, niet bij bewerken
2026-07-14 15:00:31 +02:00
Ramon
e3369e34a2 v0.3.09-beta: voeg voortgang-schema en -API toe
- 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
2026-07-14 14:51:50 +02:00
Ramon
a20779e602 v0.3.08-beta: breid widgetbibliotheek uit
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 37s
- voeg acht educatieve en klaswidgets toe\n- verbeter galerij met zoeken, favorieten en lessjablonen\n- voeg dupliceren, vergrendelen en voorbeeldweergave toe\n- voeg geschiedenis en begrensde bordexport/import toe\n- verbeter responsiviteit, toegankelijkheid en vertalingen
2026-07-14 14:16:49 +02:00
Ramon
6e41ea1b2e v0.3.07-beta: voeg educatieve widgets toe
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 29s
2026-07-14 13:39:57 +02:00
Ramon
6097d7c9f1 v0.3.06-beta: herstel bereikbaarheid via Pangolin
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
2026-07-14 13:26:15 +02:00
Ramon
50fafc789a v0.3.05-beta: rol nginx-config atomisch uit
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
2026-07-14 13:23:15 +02:00
Ramon
e727d6a07c v0.3.04-beta: draai securitytests in Docker
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 27s
2026-07-14 13:09:09 +02:00
Ramon
f9b2be8187 v0.3.03-beta: versterk publieke beveiliging
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 4s
2026-07-14 12:42:16 +02:00
Ramon
d864299ea6 v0.3.02-beta: maak tekenwerkbalk compact
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 9s
2026-07-14 10:57:56 +02:00
Ramon
09a5d84bba v0.3.01-beta: verbeter automatisch opslaan van borden 2026-07-14 10:40:58 +02:00
Ramon
4b54ea4a12 v0.3.00-beta: nieuw ontwerp \"teachus.today\" - minimalistisch, nieuw logo, licht+donker
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
- Nieuw logo: low-poly uil als SVG (transparant, vlakke merk-kleuren) + woordmerk
  \"teachus.today\" in de menubalk; op kleine schermen alleen de uil. Ook favicon
  en inlogscherm gebruiken het nieuwe logo.
- Nieuwe huisstijl afgeleid van het logo: teal als primaire actiekleur, navy inkt,
  Quicksand-font (zelf gehost, ~28 KB), dunne lijnen, vlakke minimale chrome-laag;
  het glas-effect blijft alleen op de bord-laag (widgets/HUD).
- Token-systeem herschreven: semantische kleuren (--surface/--line/--accent/...),
  radius-/schaduwschaal en één control-hoogte (--control-h) zodat alle vakken
  (inputs, selects, knoppen) in elke rij even hoog zijn; op aanraakschermen
  groeit alles mee naar 44px. Galerij-tegels hebben nu gelijke hoogtes.
- Modern tabsysteem: underline-tabs die overlopen in hun paneel (geen gele
  file-folder-tabs en omrande panelen meer); kleine keuzegroepen (taal, niveaus,
  timer, chips) delen één segmented-control-stijl.
- Donker thema: schakelbaar (licht/donker/auto volgt het OS), alle panelen,
  bordpatronen en widgets doen mee; hoog contrast wint altijd van donker.
- Nieuw tabblad Instellingen -> Weergave: thema, hoog contrast (verhuisd uit de
  menubalk), taal NL/EN (verhuisd uit de menubalk), menubalk-positie
  (boven/links/rechts), animaties aan/uit en glas-effect aan/uit - allemaal
  apparaat-gebonden en ook als gast bereikbaar via het inlogscherm.
- Menubalk: rustiger (transparante knoppen, hairline i.p.v. schaduw) en
  verplaatsbaar naar links of rechts als verticale kolom; bord, leerling-
  weergave en zwevende knoppen schuiven mee.
- Taalkeuze wordt nu onthouden (localStorage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuxHJtk5aHe2pA3xwCLEAk
2026-07-14 09:59:13 +02:00
Ramon
0411375a3c v0.2.22-beta: leerling-omgeving (aparte pagina zonder whiteboard)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 9s
- 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
2026-07-13 23:14:26 +02:00
Ramon
2528e5b8f6 v0.2.21-beta: meerdere functies per gebruiker (multi-rol voor staf) + zoekveld
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 7s
- Nieuwe user_roles-koppeltabel (db/004_user_roles.sql): staf kan voortaan naast de hoofdrol extra functies krijgen (bv. groepsleiding + schoolbeheerder tegelijk); leerling blijft altijd exclusief
- can()/PERMISSIONS accepteren nu een array van rollen i.p.v. alleen een losse string (backward compatible), CREATABLE_ROLES-opzoeking wordt een unie over alle rollen van een gebruiker
- Rechten-checks in src/api.js gebruiken voortaan req.user.allRoles (hoofd- + extra rollen); identiteits-/scoping-logica (leerling-filters, aanmaken, groepsleiding-klas-scope) blijft bewust op de hoofdrol staan
- PATCH /admin/users/:id accepteert een nieuw veld extraRoles (super-only, alleen voor staf)
- Beheerpaneel: toggle-knoppen per stafgebruiker om extra rollen aan/uit te zetten; Instellingen toont voortaan alle rollen samengevoegd ("Schoolbeheerder + Groepsleiding")
- Zoekveld boven de gebruikerslijst (live filter op naam, client-side, behoudt focus tijdens typen) omdat die lijst in een grote school lang kan worden

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 22:41:40 +02:00
Ramon
e24e810313 v0.2.20-beta: Scholen-tab wordt Overkoepelend, bugfix leerlingen-lijst, VERSION bijgewerkt
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 9s
- "Scholen"-tabblad hernoemd naar "Overkoepelend" en het aanmaken van een overkoepelend beheerder verplaatst hierheen vanuit Gebruikers - die rol hoort niet bij één school, dus dat schoolgebonden tabblad was de verkeerde plek
- Klassen-tabblad toonde een lege, onverklaarde pagina als een super nog geen school had gekozen; laat nu een duidelijke hint zien
- Bugfix: leerlingen met een klas-koppeling verdwenen onterecht helemaal uit de Gebruikers-lijst wanneer een super naar "alle scholen" keek (CLASSES was dan leeg, dus geen enkele klas-groep kon ze tonen) - valt nu terug op een platte lijst zoals bij groepsleiding
- VERSION-bestand dit keer wél meteen bijgewerkt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 22:24:16 +02:00
Ramon
2ceeec2de2 v0.2.19-beta: leerlingen per klas in het beheerpaneel + groepsleiding-scope + extra stap bij toevoegen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
- GET /admin/users: groepsleiding (teacher) ziet voortaan alleen leerlingen uit de eigen klas(sen) (via class_teachers), i.p.v. de hele school - staf blijft zichtbaar zoals voorheen
- Gebruikers-tab: leerlingen worden voor schoolbeheerder/super gegroepeerd per klas getoond (met een "geen klas"-groep), i.p.v. één platte lijst los van de Klassen-tab; groepsleiding houdt een platte lijst (nu vanzelf al beperkt tot eigen klas door de backend-scope)
- Alle "gebruiker toevoegen"-formulieren (staf + leerling, incl. de nieuwe per-klas toevoegrij) starten ingeklapt als alleen een "+ …"-knop; pas na een klik verschijnen de invoervelden
- Verwijderen van een gebruiker had al een bevestigingsvraag - ongewijzigd, bevestigd dat dit blijft werken
- Koppelcode-functie bewust ongewijzigd gelaten (privacy-optie naast direct-wachtwoord-aanmaken)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 22:04:20 +02:00
Ramon
f5140d42c9 v0.2.18-beta: VERSION-bestand was sinds v0.2.10-beta niet meer bijgewerkt
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
- Het versietagje toonde daardoor al zeven commits lang nog steeds "v0.2.10-beta", ook al draaide de echte, nieuwere code allang (Instellingen-herontwerp, beheerpaneel-tabs, ankerwoorden-plaatjes, enz.)
- VERSION teruggezet naar de daadwerkelijke huidige versie
- Robuustere weergave: op dev-builds staat de git-hash er nu altijd bij in de zichtbare tekst zelf (niet meer alleen als hover-tooltip), zodat een nieuwe deploy altijd zichtbaar anders is, ook als het VERSION-bestand een keer vergeten wordt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 21:26:42 +02:00
Ramon
1b6960b7ba v0.2.17-beta: beheerpaneel opgesplitst in tabs (Scholen/Klassen/Gebruikers)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 10s
- render() bouwde tot nu toe alles in één lange, doorlopende lijst; nu drie tabbladen die hetzelfde ed-tabs-patroon hergebruiken als de login- en Instellingen-tabs
- "Scholen" alleen zichtbaar voor super, "Gebruikers" is het standaard-tabblad
- Actief tabblad blijft behouden na acties (nieuwe klas/gebruiker toevoegen springt niet meer terug naar het eerste tabblad)
- Zuiver een frontend-herstructurering: geen gedragswijziging aan de onderliggende functionaliteit (reload/msg/classSel/userRow/addForm/resetPanel ongewijzigd)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 21:03:13 +02:00
Ramon
270739da09 v0.2.16-beta: ankerwoorden - lengtegrens-bug, geen nep-ankersymbool, koppeling met woordkaart
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 12s
- Lengte-check voor ankerwoorden was strenger dan het invoerveld zelf toestond ({2,12} vs maxLength 24): woorden als "kledingwinkel" werden nooit opgezocht terwijl ARASAAC er prima plaatjes voor heeft
- Geen ankersymbool meer als terugval wanneer ARASAAC geen match heeft (bv. "contant"); de cel blijft leeg en is klikbaar als snelkoppeling naar een nieuwe, herbruikbare plaatjeskiezer (nieuw: public/js/picker.js, hergebruikt dezelfde emoji-lijst/ARASAAC-zoekfunctie/CSS als de bestaande woordkaart-editor)
- Elk ankerraster synchroniseert zijn woorden-met-plaatje voortaan naar een eigen thema in THEMES, zodat ankerwoorden ook beheerbaar zijn via de bestaande woordkaart-editor (Letterblokken-widget) - geen wijziging aan letters.js nodig, het bestaande thema-systeem pakt dit vanzelf op
- Oudere borden met het letterlijke ankersymbool als opgeslagen "plaatje" migreren automatisch naar de nieuwe lege staat

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 19:28:05 +02:00
Ramon
6cb0965a9c v0.2.15-beta: ankerwoorden tonen nu ook zelf hun plaatje
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
- De automatische ARASAAC-plaatjes (sinds v0.2.06-beta) werden alleen doorgegeven aan andere widgets (Hak/Flits/Bal/Woordkaart) als woordbron, maar nooit getoond in het Ankerwoorden-raster zelf - vandaar dat het leek te ontbreken, voor zowel bestaande als nieuwe woorden
- render() tekent nu een klein plaatje naast elk woord in de cel, met een render()-aanroep zodra een opgehaald plaatje binnenkomt
- Getest tegen de echte ARASAAC-API: bestaande woorden (uit een opgeslagen bord) tonen hun plaatje meteen bij het laden, nieuwe woorden tonen eerst het ankersymbool en daarna het echte plaatje zodra dat binnenkomt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 17:32:42 +02:00
Ramon
baf2a4cb13 v0.2.14-beta: beheerpaneel volledig geïntegreerd in Instellingen (één overzicht)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s
- Het beheerpaneel opende nog als los venster (adminWrap) boven op Instellingen; nu is het een tabblad ("Account"/"Beheer") binnen hetzelfde Instellingen-scherm - één modal, geen twee
- adminWrap/adminModal-als-los-venster verwijderd; admin.js rendert nu in een contentblok binnen settingsModal, zonder eigen dubbele titel/sluitknop (die van Instellingen zelf dekt dat al)
- Instellingen is breder geworden om ruimte te bieden aan de beheerinhoud (klassen, gebruikers, aanmaakformulieren); het accountgedeelte blijft smal en overzichtelijk via een eigen max-breedte

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 16:23:07 +02:00
Ramon
43c7892eb7 v0.2.13-beta: één instellingenmenu - beheer, versie en copyright samengevoegd
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 9s
- Werkbalk teruggebracht tot één ⚙️-knop (was 👤 + los 👥-beheerknop + los versienummer bij het logo)
- Beheerpaneel-knop verplaatst in Instellingen (nieuwe "Beheer"-sectie, alleen zichtbaar met rechten); opent nog steeds hetzelfde beheerpaneel, sluit nu ook Instellingen zodat er geen twee schermen over elkaar staan
- Versienummer verplaatst van de werkbalk naar de voettekst van Instellingen
- Copyright-regel toegevoegd in de voettekst ("© <jaar> Ramon Besselink")
- Taal/contrast/geluid bewust NIET verplaatst: die blijven op de werkbalk omdat ze ook zonder inloggen (gastgebruik) bruikbaar moeten blijven, terwijl Instellingen alleen open gaat na inloggen

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 16:13:01 +02:00
Ramon
14115b79ad v0.2.12-beta: Instellingen herzien tot een overzichtelijk, volwassen menu
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 9s
- Losse ⚙️-knop uit de vorige commit weer verwijderd; Instellingen blijft bereikbaar via de bestaande 👤-knop, geen extra knoppen in de werkbalk
- Instellingen ingedeeld in duidelijke secties (Account, Wachtwoord, Sessie) met kaarten en sectielabels, hergebruikt van het beheerpaneel (.am-h3/.am-reset/.am-name/.am-role) i.p.v. losse velden onder elkaar
- Casual "Hoi ... 👋"-begroeting vervangen door een rustige naam/rol-weergave, consistent met hoe andere gebruikers in het beheerpaneel getoond worden
- Expliciete ✕-sluitknop toegevoegd, zelfde patroon als het beheerpaneel

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 15:55:20 +02:00
Ramon
8a3bf1e8fc v0.2.11-beta: aparte ⚙️-knop voor Instellingen in de werkbalk
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 11s
- Instellingen opende voorheen alleen via de bestaande 👤-knop, zonder enig visueel signaal in de werkbalk dat daar iets veranderd was
- Nieuwe ⚙️-knop (zichtbaar zodra je bent ingelogd) opent Instellingen rechtstreeks; de 👤-knop blijft dit ook doen zodra je bent ingelogd

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 15:46:06 +02:00
Ramon
7b6621ff9c v0.2.10-beta: echte semantische versienummering (v0.0.00-beta) i.p.v. git-sha in de UI
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
- Nieuw bestand VERSION (repo-root) is nu de enige bron voor het versienummer; CI leest dit uit en geeft het door als APP_VERSION i.p.v. het opake dev-<sha> van de vorige build
- dev-deploys tonen voortaan "v0.2.10-beta" i.p.v. "dev-<sha> · beta"; productie blijft het releaselabel tonen
- Git-sha blijft beschikbaar via /api/version (nieuw veld "sha") en als hover-title op het versietagje, voor support/debug
- Dockerfile/CI-workflows (dev + release) geven de git-sha nu als apart APP_ARG mee

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 15:30:37 +02:00
Ramon
ca825ce6f8 v0.2.09-beta: versienummer in de UI klopte niet meer met de echte build
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 13s
- #verTag toonde altijd een hardgecodeerde, verouderde versie (0.2.01); nu wordt de echte versie uit /api/version getoond zodra die bekend is (dev-<sha> · beta op dev, of het releaselabel op productie)
- versienummer weer zichtbaar op mobiel (was volledig verborgen op schermen ≤600px)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 15:23:09 +02:00
Ramon
b52c45329c v0.2.08-beta: instellingen-pagina, vernieuwd inlogscherm, centrale permissiematrix + rolwijziging
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s
- Inlogscherm herzien: aparte tabs voor "Inloggen" en "Koppelcode activeren" i.p.v. één doorlopende vorm
- Nieuwe Instellingen-pagina: account-info, wachtwoord wijzigen en uitloggen zijn verplaatst uit het inlogscherm naar een eigen scherm (👤-knop opent nu Instellingen i.p.v. het inlogscherm zodra je bent ingelogd)
- Laadstatus op de inlog-, koppelcode- en wachtwoordknoppen tijdens het versturen; consistente foutmeldingen i.p.v. een losse alert() bij het wijzigen van je eigen wachtwoord
- Centrale, benoemde permissiematrix (nieuw: src/permissions.js + public/js/permissions.js) vervangt de verspreide rol-arrays in src/api.js en de losse rol-checks in admin.js/core.js
- Nieuw: een overkoepelend beheerder (super) kan nu de rol van een stafaccount (groepsleiding/schoolbeheerder/overkoepelend) rechtstreeks in het beheerpaneel wijzigen; leerlingaccounts blijven altijd leerling

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 10:39:50 +02:00
Ramon
b80da89ab6 v0.2.07-beta: ankerwoorden automatisch opslaan
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 12s
- Ankerwoorden-widget: elk toegevoegd, gewijzigd of verwijderd woord wordt nu automatisch (na korte debounce) opgeslagen naar de server, net als bij de woordkaart-editor
- persistWords(): ververst eerst de bordstatus (stashCurrent) voordat er opgeslagen wordt, zodat ook de bestaande woordkaart-autosave geen verouderde bordstatus meer verstuurt
- Zonder deze fix gingen getypte ankerwoorden verloren bij herladen/opnieuw inloggen zonder expliciet op "Bord opslaan" te klikken, en verschenen ze daardoor ook niet in andere widgets na een reload

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 09:31:52 +02:00
Ramon
d05b12bb86 v0.2.06-beta: automatische plaatjes per woord in Ankerwoorden
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
- Ankerwoorden-widget: elk woord krijgt nu automatisch een passend plaatje via de ARASAAC-pictogrammendienst, in plaats van steeds hetzelfde ankersymbool
- Plaatjes worden per woord gecachet en bewaard in de bordstatus, zodat ze niet steeds opnieuw opgehaald worden
- Bij geen resultaat of een netwerkfout valt de widget terug op het ankersymbool
- ARASAAC-opzoekfunctie hergebruikt vanuit de bestaande woordkaart-editor (geen dubbele code meer)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 08:55:43 +02:00
Ramon
5cd1c80073 v0.2.05-beta: bronwaarde widgets als string bewaren + projectafspraken toegevoegd
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 10s
- ball.js/flits.js/hak.js: S.card bewaart de geselecteerde bronwaarde nu altijd als string i.p.v. te casten naar number, zodat de bronselectie correct blijft werken
- AGENTS.md en CLAUDE.md toegevoegd met projectafspraken

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 08:04:50 +02:00
Ramon
05d4fc7c8e v0.2.04-beta: mobiel- en tablet-vriendelijk maken van bord, widgets en beheer
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 14s
- Responsive breakpoints toegevoegd (tablet ≤1024px, telefoon ≤600px) en
  grotere tikzones op aanraakschermen (pointer:coarse) - er waren nog
  geen @media-regels in de hele stylesheet
- Bugfix: #boardsPanel had een vaste 300px-breedte en overflowde op een
  telefoon; nu net zo responsive als #boardStrip
- Widgets (letters, rekenen, schrijfblok, mindmap, ...) krijgen niet
  langer harde desktop-pixelmaten die van het scherm afvallen - grootte
  en positie worden nu geclampt aan de zichtbare bordgrootte
- Eigen, groter aanraakbare resize-greep voor widgets i.p.v. de
  piepkleine native browser-grip (resize:both)
- Beheerpaneel: dichte rijen (wachtwoordveld + knoppen) stapelen nu
  netjes op smalle schermen i.p.v. verdrukt te wrappen

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 07:53:52 +02:00
Ramon
64acb84f50 v0.2.03-beta: kopieerknop voor wachtwoorden + multireset klas/school
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 12s
- Kopieerknop (📋) bij elk getoond wachtwoord/koppelcode, zodat het niet
  meer verdwijnt voordat het overgetypt kan worden
- Bugfix: getoond wachtwoord werd bijna direct overschreven doordat de
  lijst opnieuw werd opgebouwd vlak nadat het bericht getoond werd (race
  condition tussen msg() en reload())
- Nieuw: alle leerlingwachtwoorden van een klas of hele school in één
  keer resetten, alleen voor schoolbeheerder en overkoepelend beheerder
- Resultatenpaneel na een multireset met "kopieer alles"-knop (blijft
  zichtbaar tot de beheerder het zelf sluit)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:53:08 +02:00
Ramon
bd7c7e07e7 v0.2.02: uitgebreid wachtwoordbeheer voor gebruikers
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
- Beheerders kunnen bij groepsleiding/schoolbeheer/super meteen zelf een
  wachtwoord instellen of genereren, als alternatief voor de koppelcode.
- Nieuwe directe wachtwoord-reset voor deze rollen, los van koppelcode-reset.
- Leerling-wachtwoord is na de eerste keer inloggen niet langer inzichtelijk
  voor groepsleiding/beheer; alleen opnieuw genereren kan dan nog.
- Wachtwoordeisen aangescherpt en overal consistent gecontroleerd (leerling
  min. 4 tekens, groepsleiding/beheer min. 8 met letter en cijfer).
- Bevestigingsveld en toon/verberg-knop bij accountactivatie en eigen
  wachtwoord wijzigen, om typefouten te voorkomen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:32:35 +02:00
bes-r
99fadaaedc v0.2.01: schoolsysteem + automatische migraties; deploy: APP_VERSION uit image
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 7s
2026-07-09 23:04:24 +02:00
bes-r
08fd9e11e3 v0.2.01: automatische migraties bij opstarten (transactie per bestand, geen dataverlies)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
2026-07-09 22:34:36 +02:00
bes-r
352e22a64b v0.2.00: schoolsysteem met rollen (super/admin/groepsleiding/leerling), koppelcodes, server-API en beheerpaneel
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 13s
2026-07-09 22:26:01 +02:00
bes-r
d1d3265c6c v0.1.02: opsplitsing in css/, js/core|data|board|app en js/widgets/* (geen functionele wijziging)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
2026-07-09 21:56:49 +02:00
bes-r
334b0f8e14 Versienummering en Widget Ankerwoorden toegevoegd
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 12s
2026-07-08 22:47:23 +02:00
bes-r
3a77442abc Nginx reverse proxy op poort 8081
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 21s
2026-07-08 22:17:28 +02:00
bes-r
01e7f77264 ssh deploy werkend + trigger
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 10s
2026-07-08 22:11:32 +02:00
bes-r
92a7e6e237 ssh deploy: fail-fast (BatchMode + ConnectTimeout)
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 6s
2026-07-08 22:01:23 +02:00
bes-r
2b567fe624 Deploy: alleen app-image pullen, niet Docker Hub
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 9s
2026-07-08 21:52:25 +02:00
bes-r
f2cb88f5a6 Schone pipelinerun
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Has been cancelled
2026-07-08 21:48:01 +02:00
bes-r
910f7953ad Nieuwe testrun 2026-07-08 20:49:02 +02:00
bes-r
b7979dbbcf Deploy via gewone ssh i.p.v. externe action
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Has been cancelled
2026-07-08 00:14:48 +02:00
bes-r
1db182f440 runs-on: self-hosted runner
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 2s
2026-07-07 23:45:25 +02:00
bes-r
ab16f160df Test pipeline dev->test 2026-07-07 23:35:43 +02:00
215 changed files with 30829 additions and 4877 deletions

View file

@ -13,3 +13,8 @@ POSTGRES_PASSWORD=change-me-locally
# Connectiestring die de app gebruikt.
# Hostnaam 'db' verwijst naar de postgres-service binnen het compose-netwerk.
DATABASE_URL=postgres://teach:change-me-locally@db:5432/teach
# Eerste overkoepelende beheerder (wordt eenmalig aangemaakt als er nog geen bestaat).
# Laat SUPER_PASS leeg om een willekeurig wachtwoord in de serverlog te krijgen.
SUPER_USER=beheerder
SUPER_PASS=

View file

@ -11,7 +11,7 @@ concurrency:
jobs:
build-and-deploy:
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
runs-on: dev # jouw dev-runner (test-server)
env:
# 10.0.20.22:3000 - host van je Forgejo (= container registry host)
REGISTRY: ${{ vars.REGISTRY }}
@ -19,13 +19,23 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Dependencies en securitytests
run: |
docker run --rm \
-v "$PWD:/app" \
-v /app/node_modules \
-w /app node:22-alpine \
sh -lc 'npm ci && npm test && npm audit --omit=dev --audit-level=moderate'
- name: Image-tags bepalen
id: meta
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
VERSION="$(tr -d '[:space:]' < VERSION)"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
echo "sha_tag=${IMAGE}:dev-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
echo "dev_tag=${IMAGE}:dev" >> "$GITHUB_OUTPUT"
echo "app_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Platte docker build/push i.p.v. buildx: betrouwbaarder tegen een
# HTTP-registry. De runner-host moet 10.0.20.22:3000 als
@ -34,24 +44,25 @@ jobs:
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
docker build \
--build-arg APP_VERSION="dev-${GITHUB_SHA::7}" \
--build-arg APP_VERSION="${{ steps.meta.outputs.app_version }}" \
--build-arg GIT_SHA="${GITHUB_SHA::7}" \
-t "${{ steps.meta.outputs.sha_tag }}" \
-t "${{ steps.meta.outputs.dev_tag }}" .
docker push "${{ steps.meta.outputs.sha_tag }}"
docker push "${{ steps.meta.outputs.dev_tag }}"
# Deploy via gewone ssh (geen externe action nodig).
- name: Deploy naar test-VM
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.TEST_HOST }}
username: ${{ secrets.TEST_USER }}
key: ${{ secrets.TEST_SSH_KEY }}
script: |
set -e
cd ${{ secrets.TEST_DEPLOY_PATH }}
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# Zet de te draaien image-tag in de env van de VM
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.dev_tag }}|" .env
docker compose -f compose.deploy.yaml pull
docker compose -f compose.deploy.yaml up -d
docker image prune -f
run: |
echo "${{ secrets.TEST_SSH_KEY }}" > deploy_key
chmod 600 deploy_key
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/compose.deploy.yaml \
${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }}:${{ secrets.TEST_DEPLOY_PATH }}/compose.deploy.yaml
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/nginx.conf \
${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }}:${{ secrets.TEST_DEPLOY_PATH }}/nginx.conf.new
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key ${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }} \
"set -e && cd ${{ secrets.TEST_DEPLOY_PATH }} && mv -f nginx.conf.new nginx.conf && echo '${{ secrets.REGISTRY_TOKEN }}' | docker login ${{ vars.REGISTRY }} -u '${{ secrets.REGISTRY_USER }}' --password-stdin && sed -i 's|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.dev_tag }}|' .env && docker compose -f compose.deploy.yaml pull app && docker compose -f compose.deploy.yaml up -d --force-recreate app web && docker image prune -f"
rm -f deploy_key

View file

@ -12,13 +12,21 @@ concurrency:
jobs:
build-and-deploy:
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
runs-on: prod # jouw prod-runner (productieserver)
env:
REGISTRY: ${{ vars.REGISTRY }}
IMAGE_NAME: ${{ github.repository }} # bes-r/teach
steps:
- uses: actions/checkout@v4
- name: Dependencies en securitytests
run: |
docker run --rm \
-v "$PWD:/app" \
-v /app/node_modules \
-w /app node:22-alpine \
sh -lc 'npm ci && npm test && npm audit --omit=dev --audit-level=moderate'
- name: Image-tags bepalen
id: meta
run: |
@ -35,22 +43,24 @@ jobs:
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
docker build \
--build-arg APP_VERSION="${{ steps.meta.outputs.version }}" \
--build-arg GIT_SHA="${GITHUB_SHA::7}" \
-t "${{ steps.meta.outputs.version_tag }}" \
-t "${{ steps.meta.outputs.latest_tag }}" .
docker push "${{ steps.meta.outputs.version_tag }}"
docker push "${{ steps.meta.outputs.latest_tag }}"
# Deploy via gewone ssh (geen externe action nodig).
- name: Deploy naar productie-VM
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
set -e
cd ${{ secrets.PROD_DEPLOY_PATH }}
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.version_tag }}|" .env
docker compose -f compose.deploy.yaml pull
docker compose -f compose.deploy.yaml up -d
docker image prune -f
run: |
echo "${{ secrets.PROD_SSH_KEY }}" > deploy_key
chmod 600 deploy_key
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/compose.deploy.yaml \
${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:${{ secrets.PROD_DEPLOY_PATH }}/compose.deploy.yaml
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/nginx.conf \
${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:${{ secrets.PROD_DEPLOY_PATH }}/nginx.conf.new
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }} \
"set -e && cd ${{ secrets.PROD_DEPLOY_PATH }} && mv -f nginx.conf.new nginx.conf && echo '${{ secrets.REGISTRY_TOKEN }}' | docker login ${{ vars.REGISTRY }} -u '${{ secrets.REGISTRY_USER }}' --password-stdin && sed -i 's|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.version_tag }}|' .env && docker compose -f compose.deploy.yaml pull app && docker compose -f compose.deploy.yaml up -d --force-recreate app web && docker image prune -f"
rm -f deploy_key

1
.gitignore vendored
View file

@ -8,6 +8,7 @@ npm-debug.log*
# Postgres data volume bij lokaal draaien
pgdata/
storage/
# OS / editor
.DS_Store

11
AGENTS.md Normal file
View file

@ -0,0 +1,11 @@
# Projectafspraken
- Werk nooit rechtstreeks op main
- Push naar de dev branch
- Werk met versienummering (v0.0.00)
- In de dev-branch altijd beta weergeven bij de versienummering
- Beschrijf gewijzigde functionaliteiten puntsgewijs in de pull request.
- Gebruik geen sudo
- Commit geen wachtwoorden, tokens, `.env`-bestanden of privésleutels.
- Voer relevante tests uit voor iedere commit.
- Push wijzigingen naar Forgejo, maar merge niet zelfstandig.
- Bij opdracht "Release versie" maak een versie releaseversie

11
CLAUDE.md Normal file
View file

@ -0,0 +1,11 @@
# Projectafspraken
- Werk nooit rechtstreeks op main
- Push naar de dev branch
- Werk met versienummering (v0.0.00)
- In de dev-branch altijd beta weergeven bij de versienummering
- Beschrijf gewijzigde functionaliteiten puntsgewijs in de pull request.
- Gebruik geen sudo
- Commit geen wachtwoorden, tokens, `.env`-bestanden of privésleutels.
- Voer relevante tests uit voor iedere commit.
- Push wijzigingen naar Forgejo, maar merge niet zelfstandig.
- Bij opdracht "Release versie" maak een versie releaseversie

View file

@ -13,16 +13,19 @@ ENV NODE_ENV=production
WORKDIR /app
# Draai als non-root gebruiker
RUN addgroup -S app && adduser -S app -G app
RUN addgroup -S app && adduser -S app -G app && mkdir -p /app/storage/images && chown -R app:app /app/storage
COPY --chown=app:app --from=deps /app/node_modules ./node_modules
COPY --chown=app:app package.json ./
COPY --chown=app:app src ./src
COPY --chown=app:app public ./public
COPY --chown=app:app db ./db
# Versie wordt tijdens de build meegegeven door CI
# Versie + commit-hash worden tijdens de build meegegeven door CI
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
ARG GIT_SHA=
ENV GIT_SHA=$GIT_SHA
USER app
EXPOSE 3000

View file

@ -35,8 +35,9 @@ App op http://localhost:3000, healthcheck op `/healthz`, DB-check op `/readyz`.
serveert de HTML én biedt de `/api`, en praat met de DB.
- **Forgejo container registry**: CI bouwt de image één keer en pusht die; beide
VM's pullen exact dezelfde geteste image. Geen build op de productie-VM.
- **Nginx blijft ervoor** als reverse proxy / TLS op elke VM; de app luistert
alleen op `127.0.0.1:<APP_PORT>`.
- **Pangolin/Traefik verzorgt de publieke HTTPS-ingang**. De meegeleverde nginx
vormt de interne proxylaag en luistert standaard op poort `8081`, zodat de
Pangolin/Traefik-route deze via het VM-/containernetwerk kan bereiken. De Fastify-app vertrouwt precies twee proxy-hops.
## Eenmalige setup
@ -107,28 +108,29 @@ POSTGRES_USER=teach
POSTGRES_PASSWORD=<sterk-wachtwoord>
DATABASE_URL=postgres://teach:<sterk-wachtwoord>@db:5432/teach
APP_PORT=3000
WEB_BIND_IP=0.0.0.0
WEB_PORT=8081
TRUST_PROXY_HOPS=2
SUPER_USER=beheerder
SUPER_PASS=<uniek-sterk-eerste-wachtwoord>
EOF
```
CI werkt bij elke deploy de `IMAGE=`-regel bij, pullt en herstart.
### 4. Nginx reverse proxy (per VM)
```nginx
server {
listen 443 ssl;
server_name digibord.familiebesselink.nl; # test-VM eigen subdomein
### 4. Pangolin/Traefik publiceren
# ssl_certificate ... (bestaande config)
Publiceer `http://<interne-vm-ip>:8081` via Pangolin/Traefik en laat daar TLS
beëindigen. De interne nginx behoudt `X-Forwarded-Proto: https`, waarna de app
Secure/HttpOnly/SameSite-cookies, HSTS en overige securityheaders gebruikt.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
- Beperk poort 8081 met de hostfirewall tot het Pangolin/Traefik- of tunnelnetwerk.
- Pas `TRUST_PROXY_HOPS` alleen aan wanneer de proxyketen werkelijk verandert.
- Bij een aparte Traefik-container kan een gedeeld intern Docker-netwerk nodig
zijn; zet `WEB_BIND_IP` niet ruimer dan noodzakelijk.
- `SUPER_PASS` is alleen nodig bij een lege database, wordt nooit gelogd en
moet via de beveiligde deploy-omgeving worden aangeleverd.
- De overgang naar v0.3.03-beta trekt bestaande browsersessies bewust in.
## Workflow / branching

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.4.84-beta

View file

@ -16,8 +16,14 @@ services:
PORT: 3000
DATABASE_URL: ${DATABASE_URL}
APP_VERSION: dev
SUPER_USER: ${SUPER_USER:-beheerder}
SUPER_PASS: ${SUPER_PASS:-}
TRUST_PROXY_HOPS: 0
COOKIE_SECURE: "false"
ports:
- "3000:3000"
volumes:
- image_data:/app/storage/images
restart: unless-stopped
db:
@ -39,3 +45,4 @@ services:
volumes:
pgdata:
image_data:

52
db/002_schools_auth.sql Normal file
View file

@ -0,0 +1,52 @@
-- v0.2.00: scholen, klassen, rollen en sessies.
-- Rollen: super (overkoepelend beheerder), admin (schoolbeheerder),
-- teacher (groepsleiding), pupil (leerling).
CREATE TABLE IF NOT EXISTS schools (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS classes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
school_id BIGINT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE users
ADD COLUMN IF NOT EXISTS school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
ADD COLUMN IF NOT EXISTS display_name TEXT,
ADD COLUMN IF NOT EXISTS password_hash TEXT,
-- alleen voor leerlingen: wachtwoord dat groepsleiding/beheer mag inzien
ADD COLUMN IF NOT EXISTS password_plain TEXT,
-- eenmalige koppelcode waarmee nieuwe gebruikers hun account activeren
ADD COLUMN IF NOT EXISTS link_code TEXT UNIQUE,
ADD COLUMN IF NOT EXISTS class_id BIGINT REFERENCES classes(id) ON DELETE SET NULL,
-- alle app-gegevens van de gebruiker (borden, thema's, eigen woorden)
ADD COLUMN IF NOT EXISTS data JSONB NOT NULL DEFAULT '{}'::jsonb;
-- gebruikersnaam uniek binnen een school; supers (school_id IS NULL) landelijk uniek
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_username_key;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_school_username
ON users (school_id, lower(username)) WHERE school_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_super_username
ON users (lower(username)) WHERE school_id IS NULL;
-- welke groepsleiding hoort bij welke klas
CREATE TABLE IF NOT EXISTS class_teachers (
class_id BIGINT NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY (class_id, user_id)
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_users_school ON users(school_id);
CREATE INDEX IF NOT EXISTS idx_users_class ON users(class_id);

View file

@ -0,0 +1,5 @@
-- v0.3.00: bijhouden of een account al gebruikt is, zodat leerling-
-- wachtwoorden na de eerste keer inloggen niet langer inzichtelijk zijn.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ;

9
db/004_user_roles.sql Normal file
View file

@ -0,0 +1,9 @@
-- v0.2.21: extra functies naast de hoofdrol (users.role) - alleen voor stafaccounts.
-- Leerlingen krijgen nooit een extra rol; dat wordt in de applicatielaag afgedwongen
-- (net als de bestaande regel dat alleen super rollen mag wijzigen).
CREATE TABLE IF NOT EXISTS user_roles (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL,
PRIMARY KEY (user_id, role)
);
CREATE INDEX IF NOT EXISTS idx_user_roles_user ON user_roles(user_id);

16
db/005_assignments.sql Normal file
View file

@ -0,0 +1,16 @@
-- Toewijzingen: een leerkracht koppelt één van haar eigen borden (via het
-- stabiele board-id uit users.data) aan een klas of een individuele leerling.
-- Puur additief, leeg bij aanmaak. Precies één van class_id/pupil_id gezet:
-- een klas-toewijzing (standaard voor iedereen in de klas) of een leerling-
-- toewijzing (uitzondering, voor een leerling op een ander niveau).
CREATE TABLE IF NOT EXISTS assignments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
class_id BIGINT REFERENCES classes(id) ON DELETE CASCADE,
pupil_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
board_id TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (num_nonnulls(class_id, pupil_id) = 1)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id) WHERE class_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_pupil ON assignments(pupil_id) WHERE pupil_id IS NOT NULL;

View file

@ -0,0 +1,62 @@
-- v0.3.03: tenant-isolatie en security-invarianten ook in PostgreSQL afdwingen.
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
-- Leerlingwachtwoorden worden voortaan alleen eenmalig in de API-respons getoond.
ALTER TABLE users DROP COLUMN IF EXISTS password_plain;
-- Ruim eventuele historische tenant-kruisingen veilig op vóór de triggers actief worden.
UPDATE users u SET class_id = NULL
WHERE class_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM classes c WHERE c.id = u.class_id AND c.school_id = u.school_id
);
DELETE FROM class_teachers ct
WHERE NOT EXISTS (
SELECT 1 FROM classes c
JOIN users u ON u.id = ct.user_id AND u.school_id = c.school_id
WHERE c.id = ct.class_id
AND (u.role = 'teacher' OR EXISTS (
SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher'
))
);
CREATE OR REPLACE FUNCTION teach_validate_user_class_school()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.class_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM classes c WHERE c.id = NEW.class_id AND c.school_id = NEW.school_id
) THEN
RAISE EXCEPTION 'leerling en klas moeten bij dezelfde school horen';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_users_class_school ON users;
CREATE TRIGGER trg_users_class_school
BEFORE INSERT OR UPDATE OF class_id, school_id ON users
FOR EACH ROW EXECUTE FUNCTION teach_validate_user_class_school();
CREATE OR REPLACE FUNCTION teach_validate_class_teacher()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM classes c
JOIN users u ON u.id = NEW.user_id AND u.school_id = c.school_id
WHERE c.id = NEW.class_id
AND (u.role = 'teacher' OR EXISTS (
SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher'
))
) THEN
RAISE EXCEPTION 'groepsleiding en klas moeten bij dezelfde school horen';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_class_teachers_tenant ON class_teachers;
CREATE TRIGGER trg_class_teachers_tenant
BEFORE INSERT OR UPDATE ON class_teachers
FOR EACH ROW EXECUTE FUNCTION teach_validate_class_teacher();

19
db/007_progress.sql Normal file
View file

@ -0,0 +1,19 @@
-- Voortgang: leerlingen loggen elk afgerond "moment" in een widget (ronde
-- sommen, voltooid woord, geknapte ballon) zodat leerkrachten een tijdlijn
-- per leerling kunnen bekijken. teacher_id/board_id komen server-side uit de
-- bestaande assignments-tabel, nooit van de client, om te voorkomen dat een
-- leerling voortgang onder een andere leerkracht kan wegschrijven.
CREATE TABLE IF NOT EXISTS progress_events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
board_id TEXT NOT NULL,
widget_id TEXT NOT NULL,
widget_type TEXT NOT NULL,
attempts INT NOT NULL DEFAULT 0,
correct INT NOT NULL DEFAULT 0,
stars INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_progress_events_pupil ON progress_events(pupil_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_progress_events_teacher ON progress_events(teacher_id, created_at DESC);

7
db/008_widget_share.sql Normal file
View file

@ -0,0 +1,7 @@
-- Widget-delen: een toewijzing kan naast een heel bord (widget_id NULL,
-- bestaand gedrag) ook één enkele widget van dat bord delen, met een modus:
-- 'kijken' (leerling ziet de widget read-only, snellere poll) of 'werken'
-- (leerling werkt erin; voortgang loopt via progress_events). widget_id
-- verwijst naar het stabiele wid in de borddata van de leerkracht.
ALTER TABLE assignments ADD COLUMN IF NOT EXISTS widget_id TEXT;
ALTER TABLE assignments ADD COLUMN IF NOT EXISTS mode TEXT NOT NULL DEFAULT 'werken';

16
db/009_save_system.sql Normal file
View file

@ -0,0 +1,16 @@
-- Opslaansysteem: optimistische concurrency + versiegeschiedenis.
-- data_rev telt elke wijziging van users.data; een client stuurt de rev mee
-- die hij geladen heeft en de update slaagt alleen als die nog actueel is
-- (anders 409: een ander tabblad/digibord schreef eerder). user_data_versions
-- bewaart periodieke snapshots van de vorige data zodat een leerkracht een
-- eerdere versie kan terugzetten.
ALTER TABLE users ADD COLUMN IF NOT EXISTS data_rev BIGINT NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN IF NOT EXISTS data_updated_at TIMESTAMPTZ;
CREATE TABLE IF NOT EXISTS user_data_versions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
data JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_user_data_versions ON user_data_versions(user_id, created_at DESC);

17
db/010_shared_library.sql Normal file
View file

@ -0,0 +1,17 @@
-- Gedeelde bibliotheek: borden en ankerdiagrammen die de systeemmanager
-- (school_id NULL = systeembreed, voor iedereen zichtbaar) of stafleden van
-- een school (school_id gezet = alleen binnen die school zichtbaar) hebben
-- klaargezet. Andere gebruikers kopiëren een item naar hun eigen collectie;
-- er wordt niet live in gedeelde items gewerkt. folder is een pad met
-- "/"-scheiding, dezelfde conventie als de mappenverkenner.
CREATE TABLE IF NOT EXISTS shared_library (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
kind TEXT NOT NULL,
folder TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
data JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_shared_library ON shared_library(kind, school_id);

View file

@ -0,0 +1,6 @@
-- Alleen-lezen-vlag voor gedeelde items: een vergrendeld school-item kan ook
-- door de eigenaar niet meer gewijzigd worden, alleen nog door de
-- schoolbeheerder of de systeemmanager (die de vlag ook zetten/weghalen).
-- Systeembrede items zijn impliciet al alleen-lezen voor iedereen behalve de
-- systeemmanager (beheerregel), de vlag is daar niet voor nodig.
ALTER TABLE shared_library ADD COLUMN IF NOT EXISTS readonly BOOLEAN NOT NULL DEFAULT false;

130
db/012_image_catalog.sql Normal file
View file

@ -0,0 +1,130 @@
-- v0.3.55-beta: doorzoekbare afbeeldingscatalogus met meervoudige thema's,
-- map-paden, eigenaarschap en instelbare opslagquota.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS image_quota_bytes BIGINT
CHECK (image_quota_bytes IS NULL OR image_quota_bytes >= -1);
ALTER TABLE schools
ADD COLUMN IF NOT EXISTS image_quota_bytes BIGINT
CHECK (image_quota_bytes IS NULL OR image_quota_bytes >= -1);
CREATE TABLE IF NOT EXISTS image_themes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
key TEXT UNIQUE,
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
folder TEXT NOT NULL DEFAULT '',
name_nl TEXT NOT NULL,
name_en TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (
(scope = 'global' AND school_id IS NULL) OR
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
(scope = 'personal' AND owner_id IS NOT NULL)
)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_global_name
ON image_themes (lower(name_nl)) WHERE scope = 'global';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_school_name
ON image_themes (school_id, lower(name_nl)) WHERE scope = 'school';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_personal_name
ON image_themes (owner_id, lower(name_nl)) WHERE scope = 'personal';
CREATE TABLE IF NOT EXISTS image_folders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
path TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (
(scope = 'global' AND school_id IS NULL) OR
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
(scope = 'personal' AND owner_id IS NOT NULL)
)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_global_path
ON image_folders (path) WHERE scope = 'global';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_school_path
ON image_folders (school_id, path) WHERE scope = 'school';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_personal_path
ON image_folders (owner_id, path) WHERE scope = 'personal';
CREATE TABLE IF NOT EXISTS image_assets (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
folder TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
original_name TEXT,
mime_type TEXT NOT NULL,
size_bytes BIGINT NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
storage_key TEXT UNIQUE,
public_path TEXT UNIQUE,
tags TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK ((storage_key IS NULL) <> (public_path IS NULL)),
CHECK (
(scope = 'global' AND school_id IS NULL) OR
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
(scope = 'personal' AND owner_id IS NOT NULL)
)
);
CREATE INDEX IF NOT EXISTS idx_image_assets_owner ON image_assets(owner_id);
CREATE INDEX IF NOT EXISTS idx_image_assets_school ON image_assets(school_id);
CREATE INDEX IF NOT EXISTS idx_image_assets_folder ON image_assets(scope, folder);
CREATE TABLE IF NOT EXISTS image_asset_themes (
asset_id BIGINT NOT NULL REFERENCES image_assets(id) ON DELETE CASCADE,
theme_id BIGINT NOT NULL REFERENCES image_themes(id) ON DELETE CASCADE,
PRIMARY KEY (asset_id, theme_id)
);
CREATE INDEX IF NOT EXISTS idx_image_asset_themes_theme ON image_asset_themes(theme_id);
INSERT INTO image_themes (key, scope, name_nl, name_en)
VALUES
('builtin-money', 'global', 'Geld', 'Money'),
('builtin-coins', 'global', 'Munten', 'Coins'),
('builtin-banknotes', 'global', 'Briefgeld', 'Banknotes')
ON CONFLICT (key) DO NOTHING;
INSERT INTO image_folders (scope, path)
VALUES ('global','Rekenen'), ('global','Rekenen/Geld'),
('global','Rekenen/Geld/Munten'), ('global','Rekenen/Geld/Briefgeld')
ON CONFLICT DO NOTHING;
INSERT INTO image_assets (key, scope, folder, name, mime_type, public_path, tags)
VALUES
('builtin-money-coin-1', 'global', 'Rekenen/Geld/Munten', '1 cent', 'image/png', '/img/money/coin-1.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-2', 'global', 'Rekenen/Geld/Munten', '2 cent', 'image/png', '/img/money/coin-2.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-5', 'global', 'Rekenen/Geld/Munten', '5 cent', 'image/png', '/img/money/coin-5.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-10', 'global', 'Rekenen/Geld/Munten', '10 cent', 'image/png', '/img/money/coin-10.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-20', 'global', 'Rekenen/Geld/Munten', '20 cent', 'image/png', '/img/money/coin-20.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-50', 'global', 'Rekenen/Geld/Munten', '50 cent', 'image/png', '/img/money/coin-50.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-100', 'global', 'Rekenen/Geld/Munten', '1 euro', 'image/png', '/img/money/coin-100.png', ARRAY['geld','money','munt','coin','euro']),
('builtin-money-coin-200', 'global', 'Rekenen/Geld/Munten', '2 euro', 'image/png', '/img/money/coin-200.png', ARRAY['geld','money','munt','coin','euro']),
('builtin-money-note-500', 'global', 'Rekenen/Geld/Briefgeld', '5 euro', 'image/jpeg', '/img/money/note-500.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
('builtin-money-note-1000', 'global', 'Rekenen/Geld/Briefgeld', '10 euro', 'image/jpeg', '/img/money/note-1000.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
('builtin-money-note-2000', 'global', 'Rekenen/Geld/Briefgeld', '20 euro', 'image/jpeg', '/img/money/note-2000.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
('builtin-money-note-5000', 'global', 'Rekenen/Geld/Briefgeld', '50 euro', 'image/jpeg', '/img/money/note-5000.jpg', ARRAY['geld','money','briefgeld','banknote','euro'])
ON CONFLICT (key) DO NOTHING;
INSERT INTO image_asset_themes (asset_id, theme_id)
SELECT a.id, t.id
FROM image_assets a
JOIN image_themes t ON t.key = 'builtin-money'
WHERE a.key LIKE 'builtin-money-%'
ON CONFLICT DO NOTHING;
INSERT INTO image_asset_themes (asset_id, theme_id)
SELECT a.id, t.id
FROM image_assets a
JOIN image_themes t ON t.key = CASE
WHEN a.key LIKE 'builtin-money-coin-%' THEN 'builtin-coins'
ELSE 'builtin-banknotes'
END
WHERE a.key LIKE 'builtin-money-%'
ON CONFLICT DO NOTHING;

30
db/013_parent_portal.sql Normal file
View file

@ -0,0 +1,30 @@
-- v0.3.57-beta: ouderportaal.
-- Ouders (role 'parent', school_id NULL) koppelen aan kind-accounts (pupils):
-- * parent_children: de koppeling zelf (een kind kan meerdere ouders hebben).
-- * users.parent_code: eenmalige code waarmee staf een schoolleerling aan een
-- ouder koppelt (code vervalt na gebruik).
-- * school_link_requests: aanmeldverzoek van een thuiskind (pupil zonder
-- school) bij een school; de school handelt het verzoek af (adopteren,
-- koppelen aan een bestaande leerling of afwijzen).
CREATE TABLE IF NOT EXISTS parent_children (
parent_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (parent_id, pupil_id)
);
CREATE INDEX IF NOT EXISTS idx_parent_children_pupil ON parent_children(pupil_id);
ALTER TABLE users ADD COLUMN IF NOT EXISTS parent_code TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_parent_code
ON users (parent_code) WHERE parent_code IS NOT NULL;
CREATE TABLE IF NOT EXISTS school_link_requests (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
school_id BIGINT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
parent_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (pupil_id, school_id)
);
CREATE INDEX IF NOT EXISTS idx_school_link_requests_school ON school_link_requests(school_id);

11
db/014_cms.sql Normal file
View file

@ -0,0 +1,11 @@
-- v0.3.83-beta: CMS voor de systeemmanager.
-- Kleine sleutel/waarde-opslag voor site-inhoud die zonder release aanpasbaar
-- moet zijn: welkomstwoord (inlogscherm), update-meldingen (banner) en
-- aanvullende widget-uitleg (galerij). De waarde is JSONB zodat elke sleutel
-- zijn eigen vorm heeft (bv. { nl, en } of { actief, tekst per taal, id }).
CREATE TABLE IF NOT EXISTS site_content (
key TEXT PRIMARY KEY,
value JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by BIGINT REFERENCES users(id) ON DELETE SET NULL
);

View file

@ -0,0 +1,33 @@
-- v0.3.95-beta: een gebruiker moet altijd te verwijderen zijn.
--
-- Alle foreign keys die naar users(id) verwijzen horen ON DELETE CASCADE te
-- zijn (of bewust SET NULL, zoals site_content.updated_by). Databases die zijn
-- ontstaan vóór een tabel z'n cascade kreeg houden echter de oude constraint:
-- `CREATE TABLE IF NOT EXISTS` werkt een bestaande tabel nooit bij. Op zo'n
-- database geeft het verwijderen van een gebruiker een foreign-key-fout (500
-- "serverfout"). Deze migratie zoekt elke FK naar users die NIET cascade en
-- NIET set-null is, en zet die alsnog op ON DELETE CASCADE - de bewuste
-- SET NULL-constraints blijven ongemoeid.
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT con.conname AS name,
con.conrelid::regclass::text AS tbl,
att.attname AS col
FROM pg_constraint con
JOIN pg_attribute att
ON att.attrelid = con.conrelid AND att.attnum = con.conkey[1]
WHERE con.contype = 'f'
AND con.confrelid = 'users'::regclass
AND array_length(con.conkey, 1) = 1 -- alle user-FK's zijn 1 kolom
AND con.confdeltype NOT IN ('c', 'n') -- niet cascade, niet set null
LOOP
EXECUTE format('ALTER TABLE %s DROP CONSTRAINT %I', r.tbl, r.name);
EXECUTE format(
'ALTER TABLE %s ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES users(id) ON DELETE CASCADE',
r.tbl, r.name, r.col);
RAISE NOTICE 'FK % op %(%): ON DELETE CASCADE hersteld', r.name, r.tbl, r.col;
END LOOP;
END $$;

View file

@ -0,0 +1,28 @@
-- v0.4.1-beta: een klasnaam mag niet dubbel voorkomen binnen dezelfde school.
-- Zonder deze constraint kon "Nieuwe klas" herhaaldelijk dezelfde naam
-- aanmaken (bv. na een eerdere poging die door een andere bug leek te
-- mislukken, maar stiekem toch al aanmaakte) - de app-laag controleert dit nu
-- ook (POST /admin/classes), maar de database-constraint is het echte vangnet.
--
-- Bestaande dubbele klasnamen eerst uniek maken: anders faalt de CREATE
-- UNIQUE INDEX hieronder op een database die deze bug al heeft laten
-- ontstaan (en dan start de app helemaal niet meer op). Er wordt niets
-- verwijderd, alleen de latere dubbele naam hernoemd - de klas, z'n
-- leerlingen en koppelingen blijven ongemoeid.
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT id, name,
ROW_NUMBER() OVER (PARTITION BY school_id, lower(name) ORDER BY id) AS rn
FROM classes
LOOP
IF r.rn > 1 THEN
UPDATE classes SET name = r.name || ' (' || r.rn || ')' WHERE id = r.id;
END IF;
END LOOP;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS idx_classes_school_name
ON classes (school_id, lower(name));

View file

@ -0,0 +1,44 @@
-- v0.4.07-beta: schoolbeheerder (en systeemmanager) konden niet aan een klas
-- gekoppeld worden, met een verwarrende "groepsleiding en klas moeten bij
-- dezelfde school horen" - ook al staat dit in de applicatielaag (POST
-- /admin/classes/:id/teachers, src/api.js) al langer voor elke stafrol open.
--
-- Oorzaak: de trigger teach_validate_class_teacher() (uit 006) is destijds
-- geschreven toen alleen groepsleiding aan een klas gekoppeld kon worden, en
-- is nooit meegegroeid toen schoolbeheerder/systeemmanager daar later bij
-- kwamen - de trigger stond dus in de weg van iets wat de app allang toestond.
CREATE OR REPLACE FUNCTION teach_validate_class_teacher()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM classes c
JOIN users u ON u.id = NEW.user_id
WHERE c.id = NEW.class_id
AND (
-- systeemmanager is school-onafhankelijk, mag overal aan gekoppeld worden
u.role = 'super'
OR (
u.school_id = c.school_id
AND (
u.role IN ('teacher', 'admin')
OR EXISTS (
SELECT 1 FROM user_roles ur
WHERE ur.user_id = u.id AND ur.role IN ('teacher', 'admin')
)
)
)
)
) THEN
RAISE EXCEPTION 'alleen staf uit dezelfde school (of een systeemmanager) kan aan een klas gekoppeld worden';
END IF;
RETURN NEW;
END;
$$;
-- trigger zelf blijft ongewijzigd (verwijst naar de herdefinieerde functie),
-- alleen opnieuw aangemaakt zodat deze migratie op zichzelf leesbaar is
DROP TRIGGER IF EXISTS trg_class_teachers_tenant ON class_teachers;
CREATE TRIGGER trg_class_teachers_tenant
BEFORE INSERT OR UPDATE ON class_teachers
FOR EACH ROW EXECUTE FUNCTION teach_validate_class_teacher();

6
db/018_user_avatar.sql Normal file
View file

@ -0,0 +1,6 @@
-- v0.4.23: eigen personage (avatar) per gebruiker - soort, kleuren, haar en
-- accessoires als één JSON-object. NULL = nog geen personage gekozen, de
-- client toont dan een neutrale standaardweergave.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS avatar_data JSONB;

24
db/019_pupil_levels.sql Normal file
View file

@ -0,0 +1,24 @@
-- Klassenmanagement: per vakgebied (taal/rekenen/world) een niveaugroep
-- (groep 1-8, gebaseerd op de leerjaren van de basisschool, waaraan de
-- leerdoelen gekoppeld zijn) en een niveau (1-3, fijnmazige verfijning
-- BINNEN die groep - niet aan 3 geklemd in het datamodel zelf, widgets
-- klemmen voorlopig zelf nog wel). Zelfde vorm als assignments: precies
-- één van class_id/pupil_id gezet (klas-standaard, of een individuele
-- afwijking - in het speciaal onderwijs vaak vanaf het begin de norm),
-- nu met vakgebied als extra dimensie, dus één rij per (klas|leerling,
-- vakgebied) i.p.v. één rij per klas|leerling.
CREATE TABLE IF NOT EXISTS pupil_levels (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
class_id BIGINT REFERENCES classes(id) ON DELETE CASCADE,
pupil_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
subject TEXT NOT NULL,
year_group INT NOT NULL,
level INT NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (num_nonnulls(class_id, pupil_id) = 1),
CHECK (subject IN ('taal','rekenen','world')),
CHECK (year_group BETWEEN 1 AND 8),
CHECK (level >= 1)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_pupil_levels_class ON pupil_levels(class_id, subject) WHERE class_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_pupil_levels_pupil ON pupil_levels(pupil_id, subject) WHERE pupil_id IS NOT NULL;

View file

@ -0,0 +1,7 @@
-- Voortgang: optioneel detail per moment (welke som/opdracht, gegeven
-- antwoord, juist antwoord) - tot nu toe werd alleen een totaaltelling per
-- afgerond blok bewaard (bv. math.js: één rij per 10 sommen), waardoor het
-- onmogelijk was te zien WELKE opdracht fout ging. Nullable en puur
-- additief: bestaande rijen en widgets die geen detail meegeven blijven
-- gewoon werken met detail = null.
ALTER TABLE progress_events ADD COLUMN IF NOT EXISTS detail JSONB;

24
db/021_learning_paths.sql Normal file
View file

@ -0,0 +1,24 @@
ALTER TABLE assignments
ADD COLUMN IF NOT EXISTS title TEXT,
ADD COLUMN IF NOT EXISTS sequence JSONB NOT NULL DEFAULT '[]'::jsonb;
CREATE TABLE IF NOT EXISTS assignment_step_progress (
assignment_id BIGINT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
step_id TEXT NOT NULL,
completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (assignment_id, pupil_id, step_id)
);
CREATE INDEX IF NOT EXISTS idx_assignment_step_progress_pupil
ON assignment_step_progress (pupil_id, assignment_id);
ALTER TABLE users
ADD COLUMN IF NOT EXISTS reward_points INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS user_avatar_rewards (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
reward_key TEXT NOT NULL,
unlocked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, reward_key)
);

View file

@ -0,0 +1,25 @@
-- Realtime leerlingactiviteit: een sessie start pas bij echte interactie met
-- een toegewezen widget. Heartbeats houden de sessie live; afgesloten en
-- weggevallen sessies blijven kort terug te zien in het klasdashboard.
CREATE TABLE IF NOT EXISTS live_learning_sessions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
class_id BIGINT NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
assignment_id BIGINT REFERENCES assignments(id) ON DELETE SET NULL,
board_id TEXT NOT NULL,
widget_id TEXT NOT NULL,
widget_type TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT 'werken' CHECK (mode IN ('werken', 'kijken')),
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
ended_at TIMESTAMPTZ,
progress_count INTEGER NOT NULL DEFAULT 0,
attempts INTEGER NOT NULL DEFAULT 0,
correct INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_live_learning_sessions_class_recent
ON live_learning_sessions (class_id, started_at DESC);
CREATE INDEX IF NOT EXISTS idx_live_learning_sessions_pupil_open
ON live_learning_sessions (pupil_id, ended_at, last_seen_at DESC);

View file

@ -0,0 +1,25 @@
-- Bidirectionele live klasinteractie. Sessies onthouden het laatste echte
-- voortgangsmoment en een eventuele pauze. Leerkrachtacties worden duurzaam
-- opgeslagen, zodat een korte netwerkonderbreking geen hint of aanmoediging
-- laat verdwijnen.
ALTER TABLE live_learning_sessions
ADD COLUMN IF NOT EXISTS last_progress_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS paused_at TIMESTAMPTZ;
CREATE TABLE IF NOT EXISTS teacher_pupil_actions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
class_id BIGINT NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('hint', 'encourage', 'pause', 'resume', 'easier')),
message TEXT,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
acknowledged_at TIMESTAMPTZ,
CHECK (message IS NULL OR char_length(message) <= 160)
);
CREATE INDEX IF NOT EXISTS idx_teacher_pupil_actions_pending
ON teacher_pupil_actions (pupil_id, acknowledged_at, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_teacher_pupil_actions_class
ON teacher_pupil_actions (class_id, created_at DESC);

View file

@ -0,0 +1,7 @@
-- Kleurplaten gebruiken dezelfde klas-standaard-met-leerling-override als de
-- andere vakgebieden. Bestaande niveaukeuzes blijven ongewijzigd; alleen de
-- toegestane waarde 'creatief' wordt aan de constraint toegevoegd.
ALTER TABLE pupil_levels DROP CONSTRAINT IF EXISTS pupil_levels_subject_check;
ALTER TABLE pupil_levels
ADD CONSTRAINT pupil_levels_subject_check
CHECK (subject IN ('taal','rekenen','world','creatief'));

View file

@ -19,10 +19,29 @@ services:
environment:
PORT: 3000
DATABASE_URL: ${DATABASE_URL}
APP_VERSION: ${IMAGE}
# APP_VERSION komt uit de image zelf (build-arg); hier niet overschrijven.
SUPER_USER: ${SUPER_USER:-beheerder}
SUPER_PASS: ${SUPER_PASS:-}
# Pangolin/Traefik -> nginx -> app: twee vertrouwde proxy-hops.
TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-2}
COOKIE_SECURE: "true"
ports:
# Alleen op localhost van de VM; nginx zit ervoor als reverse proxy
- "127.0.0.1:${APP_PORT:-3000}:3000"
volumes:
- image_data:/app/storage/images
restart: unless-stopped
web:
image: nginx:alpine
depends_on:
- app
ports:
# Bereikbaar voor Pangolin/Traefik via het VM-/containernetwerk.
# Beperk poort 8081 met de hostfirewall tot de proxy/tunnel.
- "${WEB_BIND_IP:-0.0.0.0}:${WEB_PORT:-8081}:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
db:
@ -43,3 +62,4 @@ services:
volumes:
pgdata:
image_data:

57
deploy/nginx.conf Normal file
View file

@ -0,0 +1,57 @@
# Behoud het publieke schema van Pangolin/Traefik; val lokaal terug op nginx.
map $http_x_forwarded_proto $teach_forwarded_proto {
default $http_x_forwarded_proto;
"" $scheme;
}
server {
listen 80;
server_name _;
server_tokens off;
# Wat groter zodat grote borden/afbeeldingen niet geweigerd worden
client_max_body_size 55m;
# Gzip-vangnet: de app comprimeert zelf al (brotli/gzip via @fastify/compress)
# en nginx comprimeert nooit dubbel (responses mét Content-Encoding blijven
# ongemoeid) - dit vangt alleen wat de app mist. Let op: zonder
# gzip_proxied comprimeert nginx geproxyde responses helemaal niet.
gzip on;
gzip_proxied any;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml text/plain;
location /api/my/live-actions/ {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $teach_forwarded_proto;
}
location /api/progress/live/ {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $teach_forwarded_proto;
}
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $teach_forwarded_proto;
}
}

View file

@ -0,0 +1,318 @@
# Curriculumkader kerndoelen primair onderwijs 2026
Laatst inhoudelijk gecontroleerd: 19 juli 2026
Broneditie: SLO, *Kerndoelen primair onderwijs*, versie juli 2026
Status van dit document: implementatiegericht werkdocument voor Teach
## Doel en afbakening
Dit document ordent de nieuwe kerndoelen voor het primair onderwijs per leergebied. De formuleringen hieronder zijn compacte samenvattingen voor productontwikkeling; de officiële publicatie blijft leidend voor interpretatie, ontwerp en verantwoording.
De juli-publicatie bevat veertig kerndoelen in negen leergebieden. SLO duidt ze nog aan als definitieve conceptkerndoelen: het ministerie van OCW legt ze vast in wet- en regelgeving. Volgens de actuele SLO-informatie verloopt de invoering gefaseerd en moeten alle scholen uiterlijk in augustus 2031 op de nieuwe kerndoelen zijn overgegaan.
Dit kader omvat de landelijke po-doelen. De aparte doelen voor Friese taal en cultuur en Nederlandse Gebarentaal vallen buiten deze inventarisatie en vragen later een eigen uitbreiding wanneer Teach die doelgroepen ondersteunt.
## TULE, inhoudslijnen en de nieuwe leerlijnen
TULE beschrijft een mogelijke uitwerking van de kerndoelen uit 2006 over groep 1 tot en met 8. Die uitwerking is voorbeeldmatig en geen methode, verplicht programma of bewijs dat een leerling een doel beheerst. De bijbehorende inhoudslijnen ordenen aanbod in drie fasen: jonge kind, fase 2 en fase 3.
De oude TULE-structuur kent zeven leergebieden: Nederlands, Engels, Friese taal, rekenen/wiskunde, oriëntatie op jezelf en de wereld, kunstzinnige oriëntatie en bewegingsonderwijs. De nieuwe landelijke set is anders geordend en voegt onder meer burgerschap en digitale geletterdheid als zelfstandige leergebieden toe. Ook wordt het oude brede gebied oriëntatie op jezelf en de wereld verdeeld over mens en maatschappij en mens en natuur.
SLO ontwikkelt nieuwe leerlijnen bij de kerndoelen van 2026. Nederlands en rekenen/wiskunde zijn in maart 2026 geconsulteerd. Voor de overige zeven leergebieden is de consultatie volgens de huidige planning voorzien voor april 2027. Daarom geldt voor Teach:
- gebruik de doelen uit 2026 als actuele inhoudelijke hoofdstructuur;
- gebruik TULE alleen als historische inspiratie voor opbouw en werkvormen;
- presenteer een zelfgemaakte niveau-indeling nooit als officiële SLO-leerlijn;
- voeg de nieuwe SLO-leerlijnen later als afzonderlijke, geversioneerde laag toe.
## Lees- en modelleringsregels
Een officieel kerndoel bestaat uit een kernzin, een of meer doelzinnen en een nadere uitwerking. De actor is de school of de leerling. Daarnaast onderscheidt SLO aanbod, beheersing, ervaring en combinaties daarvan. Dat verschil is essentieel voor widgets:
- een schooldoel mag niet als individuele leerlingprestatie worden gescoord;
- een beheersingsdoel kan bewijs opleveren via handelen, een product of een antwoord;
- een ervaringsdoel vraagt deelname, reflectie of expressie en geen simpele goed/fout-score;
- een widget die één onderdeel oefent, dekt niet automatisch het volledige kerndoel;
- activiteit, leerdoel en verzameld bewijs moeten expliciet op elkaar aansluiten.
De kolom **Teach nu** gebruikt drie waarden:
- **Deel**: een huidige widget oefent aantoonbaar een deel van het doel;
- **Steun**: een huidige widget kan de les ondersteunen, maar bevat geen volwaardige doelgerichte activiteit;
- **Open**: er is nog geen specifieke widgetdekking.
Geen van deze labels is een formele SLO-validering.
## Overzicht
| Leergebied | Kerndoelen | Domeinen |
| --- | ---: | --- |
| Nederlands | 19 | overkoepelend; communicatie; taal; literatuur |
| Rekenen en wiskunde | 1018 | wiskundige concepten; denk-werkwijzen; wiskunde en de wereld |
| Burgerschap | 1921 | democratische oefenplaats; democratische rechtsstaat; betrokkenheid |
| Digitale geletterdheid | 2224 | kennis en vaardigheden; ontwerpen en maken; gedigitaliseerde wereld |
| Mens en maatschappij | 2528 | vraagstukken en perspectieven; ruimte; tijd; samenleving |
| Mens en natuur | 2932 | natuurwetenschappen en technologie; natuur- en scheikunde/techniek; organismen en gezondheid; systeem aarde |
| Moderne vreemde talen: Engels | 3334 | communicatie; taal- en cultuurbewustzijn |
| Kunst en cultuur | 3537 | artistiek creatief vermogen; maken; meemaken |
| Bewegen en sport | 3840 | leren bewegen; samen bewegen; betrokken bewegen |
## 1. Nederlands
Bedoeling: leerlingen ontwikkelen taalcompetentie in samenhangende, betekenisvolle situaties. Begrijpen, produceren, gesprekken voeren, kennis van het taalsysteem en ervaringen met literatuur horen bij elkaar. Taalontwikkeling vindt bovendien in alle leergebieden plaats.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 1 | Overkoepelend | De school bouwt taalcompetentie op via een rijke taal- en leesomgeving (1A) en taalontwikkeling in alle leergebieden (1B). | **Steun** — alle taalwidgets, anchor, media |
| 2 | Communicatie | Leerlingen begrijpen zakelijke en literaire teksten (2A), vergelijken en evalueren teksten voor dieper begrip (2B) en onderzoeken bronbetrouwbaarheid (2C). | **Steun** — anchor, mind, media |
| 3 | Communicatie | Leerlingen spreken en schrijven passend bij doel, publiek en context (3A), gebruiken taal creatief (3B) en schrijven om kennis en gedachten te ordenen (3C). | **Deel** — sentence, write, notes, mind |
| 4 | Communicatie | Leerlingen voeren doelgerichte gesprekken (4A) en gebruiken gesprekken om samen te denken en leren (4B). | **Steun** — anchor, mind, poll |
| 5 | Communicatie | Leerlingen kijken terug op taalactiviteiten, aanpak en ontwikkeling en leren bewust keuzes maken (5A). | **Steun** — taalwidgets geven feedback, maar vragen nog geen inhoudelijke reflectie |
| 6 | Taal | Leerlingen onderzoeken relaties tussen vorm en betekenis (6A) en passen spelling, formulering en interpunctie functioneel toe (6B). | **Deel** — letters, hak, sentence, wordtypes |
| 7 | Taal | Leerlingen onderzoeken hun talige identiteit en repertoire (7A) en variatie en verandering in taal (7B). | **Steun** — tweetalige woordcatalogus en taalkeuze |
| 8 | Literatuur | Leerlingen bouwen een eigen leesvoorkeur op (8A) en ervaren welke persoonlijke en maatschappelijke waarde literatuur kan hebben (8B). | **Open** — media kan alleen materiaal tonen |
| 9 | Literatuur | Leerlingen doorgronden verhalende teksten (9A) en herkennen en bespreken kenmerken van literaire genres (9B). | **Open** |
Ontwerpaccenten:
- breid taalwidgets uit van losse woorden en zinnen naar rijke teksten, bronnen en gesprekken;
- laat bron, doel, publiek en context als instelbare metadata terugkomen;
- voeg bij taalproducten een reflectiestap toe zonder creativiteit alleen met goed/fout te beoordelen;
- bouw een tekst- en literatuurwidget voor doelen 2, 8 en 9.
## 2. Rekenen en wiskunde
Bedoeling: leerlingen leren wiskundige concepten begrijpen en gebruiken, passende denk-werkwijzen inzetten en wiskunde herkennen en toepassen in de werkelijkheid en andere leergebieden.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 10 | Wiskundige concepten | Redeneren en rekenen met gehele en decimale getallen (10A), breuken (10B) en verhoudingen, procenten en schaal (10C). | **Deel** — madd, msub, mmul, mdiv, mpct, numberline, fractions, placevalue |
| 11 | Wiskundige concepten | Meten, redeneren en rekenen met grootheden, eenheden en passende meetinstrumenten (11A). | **Deel** — clock, money; andere grootheden zijn open |
| 12 | Wiskundige concepten | Data verzamelen of ordenen, representeren, interpreteren en conclusies kritisch beoordelen (12A). | **Steun** — poll; nog geen leerlinggerichte data-analyse |
| 13 | Wiskundige concepten | Patronen en verbanden herkennen, beschrijven, voortzetten en met passende representaties onderzoeken (13A). | **Open** |
| 14 | Wiskundige concepten | Redeneren over figuren en plaatsbepaling en meetkundige transformaties uitvoeren (14A). | **Open** — write met raster is alleen gereedschap |
| 15 | Wiskundige denk-werkwijzen | Problemen oplossen (15A), situaties modelleren (15B) en algoritmes gebruiken en beschrijven (15C). | **Steun** — rekenwidgets oefenen uitkomsten, maar leggen aanpak en model nog nauwelijks vast |
| 16 | Wiskundige denk-werkwijzen | Wiskundetaal en representaties gebruiken en omzetten (16A), en wiskundige instrumenten beredeneerd kiezen en inzetten (16B). | **Deel** — numberline, fractions, clock, placevalue, money |
| 17 | Wiskunde en de wereld | Een onderzoekende houding ontwikkelen: precies, kritisch, volhardend en met vertrouwen wiskundig handelen (17A). | **Steun** — feedback en niveaus ondersteunen oefenen, maar meten de attitude niet |
| 18 | Wiskunde en de wereld | Wiskunde herkennen en toepassen in echte situaties (18A) en in andere leergebieden (18B). | **Deel** — money, clock, topography |
Ontwerpaccenten:
- voeg modules toe voor meten, data, patronen en meetkunde;
- registreer naast het antwoord ook gekozen representatie, hulpmiddel en eventueel strategie;
- gebruik realistische contexten zonder taalbelasting onnodig te verhogen;
- behandel een vlotte rekenscore niet als bewijs voor probleemoplossen of modelleren.
## 3. Burgerschap
Bedoeling: de school is een veilige democratische oefenplaats. Leerlingen leren de basis van de democratische rechtsstaat en diversiteit kennen en doen ervaring op met gezamenlijke besluitvorming en maatschappelijke betrokkenheid.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 19 | Democratische oefenplaats | De school stimuleert sociale en maatschappelijke competenties en respectvolle, veilige interactie, offline en online (19A). | **Steun** — poll, mood, feelings, compliments, names, light |
| 20 | Samenleven in een democratische rechtsstaat | Leerlingen onderzoeken democratische basiswaarden en rechtsstatelijke beginselen (20A) en diversiteit in de samenleving (20B). | **Steun** — sociaal-emotionele widgets; inhoud over rechtsstaat ontbreekt |
| 21 | Democratische en maatschappelijke betrokkenheid | Leerlingen ervaren democratische deelname en invloed (21A) en maatschappelijke inzet en samenwerking (21B). | **Steun** — poll, mind, anchor; geen project- of actieworkflow |
Ontwerpaccenten:
- ontwerp een klasraad- of besluitvormingswidget met rollen, argumenten, stemvormen en reflectie;
- voorkom dat meerderheid van stemmen als enige vorm van democratie wordt gepresenteerd;
- leg bij doel 19 bewijs op school- of lesniveau vast, niet als leerlingranglijst;
- maak ruimte voor meerdere perspectieven, belangen, basiswaarden en minderheidsstemmen.
## 4. Digitale geletterdheid
Bedoeling: leerlingen leren digitale systemen en media functioneel en kritisch gebruiken, digitale producten ontwerpen en deelnemen aan de gedigitaliseerde wereld met aandacht voor veiligheid, privacy, zichzelf, anderen en de samenleving.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 22 | Praktische kennis en vaardigheden | Digitale systemen functioneel inzetten (22A), doelgericht informatie zoeken en beoordelen (22B), data gebruiken (22C) en AI herkennen, gebruiken en kritisch onderzoeken (22D). | **Steun** — gebruik van Teach, media, mind, write; geen expliciete leerlijnactiviteit |
| 23 | Ontwerpen en maken | Digitale producten iteratief creëren (23A) en programmeerbare oplossingen ontwerpen, testen en verbeteren (23B). | **Steun** — bord, tekenen, write, mind, media; programmeren ontbreekt |
| 24 | De gedigitaliseerde wereld | Veiligheid en privacy afwegen (24A), invloed op jezelf en anderen onderzoeken (24B) en wisselwerking met samenleving en wereld beoordelen (24C). | **Open** |
Ontwerpaccenten:
- maak een digitale werkplaats met bestandstaken, zoeken, bronbeoordeling, data en AI-geletterdheid;
- voeg een blok- of stappenprogrammeermodule toe waarin ontwerpen, testen en verbeteren zichtbaar zijn;
- gebruik gesimuleerde persoonsgegevens en veilige scenarios; verzamel geen gevoelige leerlingdata om privacy te onderwijzen;
- scheid productvaardigheid van kritisch begrip van digitale technologie.
## 5. Mens en maatschappij
Bedoeling: leerlingen onderzoeken de menselijke leefwereld vanuit geografische, historische, economische en sociale invalshoeken. Ze leren vraagstukken, bronnen, perspectieven, waarden, belangen, ruimte, tijd en samenleving met elkaar verbinden.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 25 | Vraagstukken en perspectieven | Maatschappelijke verschijnselen analyseren met meerdere denkwijzen (25A), vraagstukken vanuit bronnen en perspectieven onderzoeken (25B) en de schoolomgeving geografisch, historisch, economisch en sociaal verkennen (25C). | **Steun** — mind, anchor, poll, media |
| 26 | Mens en ruimte | Een wereldbeeld opbouwen met geografische bronnen, topografie en persoonlijke betekenis (26A), en wederzijdse invloed van menselijk handelen en leefomgeving onderzoeken (26B). | **Deel** — topography; sociale geografie en bronanalyse zijn nog beperkt |
| 27 | Mens en tijd | Historische ontwikkelingen en samenlevingen onderzoeken (27A), chronologie en tijdsindeling gebruiken (27B) en doorwerking van het verleden herkennen (27C). | **Open** |
| 28 | Mens en samenleving | Economische keuzes en gevolgen onderzoeken (28A), invloed en macht herkennen (28B), diversiteit begrijpen (28C), veilig en respectvol deelnemen aan de sociale omgeving (28D) en verantwoord deelnemen aan verkeer (28E). | **Steun** — money, sociaal-emotionele widgets en topography; doelgerichte inhoud ontbreekt grotendeels |
Ontwerpaccenten:
- behoud topography als oefenwidget, maar voeg kaarten als bron en verklarende vragen toe;
- bouw een tijdlijn- en bronnenwidget voor geschiedenis;
- gebruik één herbruikbare onderzoeksschil voor vraagstuk, bron, perspectief, waarde/belang, conclusie en presentatie;
- behandel verkeer, economie en sociale ontwikkeling als inhoudelijke leeractiviteiten, niet alleen als thema of illustratie.
## 6. Mens en natuur
Bedoeling: leerlingen onderzoeken natuurwetenschappelijke verschijnselen, technologie, organismen, gezondheid en systeem aarde. Waarnemen, vragen stellen, onderzoeken, ontwerpen, verklaren en waarderen worden in samenhang aangeboden.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 29 | Natuurwetenschappen en technologie | Vraagstukken in de leefomgeving onderzoeken (29A), natuurwetenschappelijke en technologische denkwijzen gebruiken (29B), onderzoek- en ontwerpwerkwijzen uitvoeren (29C) en begrijpen hoe kennis en techniek tot stand komen (29D). | **Deel** — trees, birds, plants ondersteunen waarnemen en classificeren; onderzoek en ontwerpen zijn open |
| 30 | Natuurkundige en scheikundige verschijnselen en technische systemen | Werking en ontwerp van technische systemen onderzoeken (30A), stoffen en materiaaleigenschappen verkennen (30B) en verschijnselen rond licht, geluid, energie en krachten onderzoeken (30C). | **Open** |
| 31 | Organismen en gezondheid | Bouw, functies, levensprocessen en ordening van organismen onderzoeken (31A) en keuzes rond lichamelijke en mentale gezondheid begrijpen (31B). | **Deel** — trees, birds, plants; breathe, mood en feelings zijn alleen ondersteunend bij welzijn |
| 32 | Systeem aarde | Opbouw en verandering van de aarde onderzoeken (32A), weer, klimaat en waterkringloop begrijpen (32B) en relaties, biodiversiteit en menselijke invloed in ecosystemen verklaren (32C). | **Deel** — soortwidgets en topography; systemen, klimaat en voedselrelaties ontbreken |
Ontwerpaccenten:
- voeg aan de soortwidgets taxonomie, kenmerkenvergelijking, voedselrelaties en leefgebieden toe;
- bouw een onderzoekslab voor voorspellen, waarnemen/meten, resultaten vastleggen en verklaren;
- voeg veilige simulaties toe voor krachten, energie, stoffen, weer, water en ecosystemen;
- onderscheid feitkennis, onderzoeksvaardigheid en waardering van natuur in activiteiten en voortgang.
## 7. Moderne vreemde talen: Engels
Bedoeling: leerlingen ontwikkelen plezier en vertrouwen in eenvoudige communicatie in het Engels en worden zich bewust van hun taalleerproces, meertaligheid en culturele contexten.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 33 | Communicatie | Eenvoudige Engelse teksten bekijken, beluisteren en lezen (33A), mondeling communiceren (33B) en eenvoudige schriftelijke communicatie gebruiken (33C). | **Deel** — tweetalige woordcatalogus, letters, flits, hak, ball, spraakondersteuning; gesprekken en rijke teksten zijn beperkt |
| 34 | Taal- en cultuurbewustzijn | Reflecteren op taalleren en meertaligheid (34A) en overeenkomsten, verschillen en passende communicatie in culturele contexten onderzoeken (34B). | **Steun** — taalwissel en tweetalige inhoud; interculturele activiteit ontbreekt |
Ontwerpaccenten:
- voeg luisterfragmenten, korte dialogen en functionele spreek- en schrijftaken toe;
- gebruik vertrouwde contexten en geleidelijke taalsteun;
- maak ruimte voor thuistalen en vergelijking zonder talen of accenten te rangschikken;
- voeg culturele context toe als onderzoek en ontmoeting, niet als verzameling stereotypen.
## 8. Kunst en cultuur
Bedoeling: leerlingen ontwikkelen artistiek creatief vermogen, maken eigen kunstzinnige uitingen en beleven en duiden kunst en cultuur in verschillende contexten.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 35 | Artistiek creatief vermogen | In een iteratief maakproces creatieve strategieën gebruiken (35A), verbeelding en artistieke mogelijkheden ontwikkelen (35B) en betekenis geven aan eigen en andermans werk (35C). | **Steun** — tekenen, write, mind, media; geen begeleid artistiek proces |
| 36 | Maken en betekenis geven | Ideeën artistiek verbeelden en creëren (36A) en technieken, vaardigheden, materialen en vaktaal uit verschillende disciplines inzetten (36B). | **Steun** — bordtekenen, write, media; muziek, drama, dans en gerichte technieken ontbreken |
| 37 | Meemaken en betekenis geven | Kunst en cultuur actief beleven (37A) en verbinden aan maker, tijd, plaats, functie en andere context (37B). | **Open** — media kan bronnen tonen maar begeleidt beleving en duiding niet |
Ontwerpaccenten:
- ontwerp een creatief atelier met inspiratie, experiment, versie, presentatie en reflectie;
- ondersteun beeld, muziek, beweging, drama en combinaties daarvan;
- beoordeel proces, keuzes en betekenis; voorkom één norm voor wat een goed kunstwerk is;
- koppel bronnen aan maker, context, rechten en herkomst.
## 9. Bewegen en sport
Bedoeling: leerlingen ontwikkelen beweegvaardigheid, leren veilig en verantwoordelijk met anderen bewegen en onderzoeken welke beweegvormen, contexten en motieven bij hen passen.
| ID | Domein | Samenvatting en onderdelen | Teach nu |
| --- | --- | --- | --- |
| 38 | Leren bewegen | Deelnemen aan uiteenlopende beweegthemas (38A), beweegvaardigheid verbeteren (38B) en activiteiten passend maken bij eigen mogelijkheden (38C). | **Open** — timer, dice en names kunnen alleen organiseren |
| 39 | Samen bewegen | Verantwoordelijkheid nemen voor veilig en sociaal samen bewegen (39A) en activiteiten helpen opbouwen, begeleiden en regelen (39B). | **Open** — algemene klaswidgets bieden hooguit ondersteuning |
| 40 | Betrokken bewegen | Verschillende beweegcontexten, doelen en relaties met welbevinden verkennen (40A) en eigen voorkeuren, kwaliteiten en ontwikkeling onderzoeken (40B). | **Open** |
Ontwerpaccenten:
- maak een beweegactiviteitenwidget met veilige instructie, differentiatie en organisatievormen;
- registreer deelname en reflectie terughoudend; vermijd openbare ranglijsten en medische conclusies;
- laat leerlingen rollen kiezen en regels of materiaal passend aanpassen;
- ontwerp voor gebruik naast het bord: grote bediening, weinig leestijd en duidelijke fasen.
## Dekkingsbeeld en prioriteiten
Teach is op dit moment het sterkst in onderdelen van Nederlands, rekenen/wiskunde, topografie en organismenkennis. De grootste inhoudelijke hiaten zijn literatuur en bronbegrip, data/patronen/meetkunde, geschiedenis, natuurkundig en technologisch onderzoek, digitale geletterdheid, kunstbeleving en bewegen en sport.
Aanbevolen volgorde:
1. Voeg eerst curriculummetadata en doelkoppelingen toe zonder bestaand widgetgedrag te wijzigen.
2. Verdiep bestaande taal-, reken- en wereldwidgets waar zij al een duidelijk deeldoel raken.
3. Bouw herbruikbare schillen voor tekst/bronnen, onderzoeken en creëren.
4. Voeg ontbrekende inhoudswidgets toe voor geschiedenis, burgerschap, digitale geletterdheid, kunst en bewegen.
5. Verwerk officiële nieuwe SLO-leerlijnen pas na publicatie als een apart geversioneerd gegevenspakket.
## Technisch gegevensmodel voor widgets
Galerijcategorieën (taal, rekenen, world, class, tools en media) blijven bedoeld voor vindbaarheid. Curriculumkoppelingen worden een aparte many-to-many-laag, omdat bijvoorbeeld een poll tegelijk rekenen/data, taal/gesprek en burgerschap kan ondersteunen.
Aanbevolen doelrecord:
{
id: "slo-po-2026-10a",
framework: "slo-po-2026",
edition: "2026-07",
legalStatus: "definitief-concept",
learningArea: "rekenen-en-wiskunde",
domain: "wiskundige-concepten",
goal: 10,
component: "a",
actor: "leerling",
goalType: "beheersing",
summary: "Redeneren en rekenen met gehele en decimale getallen.",
source: {
publicationPage: 33,
url: "https://www.slo.nl/publish/pages/23186/kerndoelen-primair-onderwijs-slo-juli-2026.pdf"
}
}
Het veld goalType en de officiële detailinhoud moeten bij invoer per doelzin tegen de bron worden gecontroleerd; neem ze niet automatisch over uit het werkwoord van de samenvatting.
Aanbevolen widgetkoppeling:
{
widgetId: "fractions",
goalId: "slo-po-2026-10b",
relation: "exercise",
coverage: "partial",
activityModes: ["explore"],
evidence: ["constructed-representation"],
teacherNote: "Oefent breukrepresentaties; dekt niet alle relaties en berekeningen uit 10B."
}
Gebruik voor relation minimaal:
- exercise: de leerling handelt gericht met inhoud uit het doel;
- support: de widget ondersteunt instructie of gesprek;
- evidence: de activiteit kan passend bewijs van leren opleveren;
- context: het doel levert alleen een betekenisvolle context.
Voor voortgang zijn naast goalId ook activiteit, niveau, poging, resultaatsoort en bewijssoort nodig. Een numerieke score past niet bij elk doel. Bewaar bij open producten liever een productverwijzing, observatie of reflectie. Schooldoelen krijgen een aanbodregistratie op klas- of schoolniveau.
De huidige voortgangstaxonomie gebruikt hoofdzakelijk taal, rekenen en world. Breid die niet ad hoc uit met widgetcategorieën. Introduceer stabiele codes voor alle negen leergebieden en behoud aliassen voor bestaande gegevens:
nederlands
rekenen-en-wiskunde
burgerschap
digitale-geletterdheid
mens-en-maatschappij
mens-en-natuur
engels
kunst-en-cultuur
bewegen-en-sport
## Acceptatiecriteria voor curriculumkoppelingen
Een koppeling tussen widget en doel is pas gereed wanneer:
- doel-ID, broneditie, domein en actor zijn vastgelegd;
- duidelijk is welk onderdeel van het doel wordt geoefend of ondersteund;
- activiteit en feedback passen bij aanbod, beheersing of ervaring;
- dekkingsclaim expliciet partial of aantoonbaar vollediger is;
- niveau-opbouw als Teach-keuze of als officiële leerlijn is gelabeld;
- toegankelijkheid, inclusiviteit, privacy en mobiele bediening zijn beoordeeld;
- tests controleren dat doel-IDs geldig en bronversies bekend zijn;
- voortgang geen bredere beheersing suggereert dan het verzamelde bewijs toelaat.
## Bronnen
- SLO, [Kerndoelen primair onderwijs — versie juli 2026](https://www.slo.nl/publish/pages/23186/kerndoelen-primair-onderwijs-slo-juli-2026.pdf). Primaire bron voor de veertig doelen.
- SLO, [Kerndoelen primair onderwijs — publicatiepagina](https://www.slo.nl/publicaties/@25212/kerndoelen-primair-onderwijs/). Status en actuele editie.
- SLO, [Kerndoelen primair onderwijs — sectorinformatie](https://www.slo.nl/sectoren/po/kerndoelen/). Invoering, huidige doelen en relatie met TULE.
- SLO, [TULE inhouden en activiteiten](https://www.slo.nl/thema/meer/tule/). Historische uitwerking van de doelen uit 2006.
- SLO, [Inhoudslijnen primair onderwijs](https://www.slo.nl/sectoren/po/inhoudslijnen-po/). Fasering van de bestaande inhoudslijnen.
- SLO, [Ontwikkeling leerlijnen](https://www.slo.nl/thema/meer/kerndoelen-onderwijs/ontwikkeling-leerlijnen/). Status en planning van leerlijnen bij de nieuwe doelen.
- SLO, [Wijzigingen kerndoelenbundels](https://www.slo.nl/thema/meer/actualisatie-kerndoelen-examenprogramma/actualisatie-kerndoelen/wijzigingen-bundels-kerndoelen/). Correcties in de editie van juli 2026.

659
package-lock.json generated
View file

@ -8,7 +8,11 @@
"name": "teach",
"version": "0.1.0",
"dependencies": {
"@fastify/static": "^8.0.1",
"@fastify/compress": "^8.3.1",
"@fastify/cookie": "^11.1.1",
"@fastify/helmet": "^13.1.0",
"@fastify/rate-limit": "^11.1.0",
"@fastify/static": "^10.1.2",
"fastify": "^5.1.0",
"pg": "^8.13.1"
},
@ -53,6 +57,81 @@
"fast-uri": "^3.0.0"
}
},
"node_modules/@fastify/compress": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-8.3.1.tgz",
"integrity": "sha512-BUpItLr6MUX9e9ukg5Y6xekyA/7pBFG8QWtFCrUDm9ctoBc3R2/nA16yOaOWtVoccpXGjdDEYA/MxAb5+8cxag==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@fastify/accept-negotiator": "^2.0.0",
"fastify-plugin": "^5.0.0",
"mime-db": "^1.52.0",
"minipass": "^7.0.4",
"peek-stream": "^1.1.3",
"pump": "^3.0.0",
"pumpify": "^2.0.1",
"readable-stream": "^4.5.2"
}
},
"node_modules/@fastify/compress/node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/cookie": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.1.tgz",
"integrity": "sha512-sJ0NXzGVYjUB4OynPZRsIcQ1mKSP4rW45xLCN0aelRq5Vl37xVVbz5kJ6Y0a9m2T0mCUjYCuvlUA9QlTafrZWw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"cookie": "^2.0.0",
"fastify-plugin": "^6.0.0"
}
},
"node_modules/@fastify/cookie/node_modules/cookie": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
"integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==",
"license": "MIT",
"engines": {
"node": ">=22"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@fastify/error": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
@ -104,6 +183,26 @@
],
"license": "MIT"
},
"node_modules/@fastify/helmet": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-13.1.0.tgz",
"integrity": "sha512-SvVOU0IrzYJW1BvSkfq9G1WUdW3dnaRUvg6m0BtgGMBmML62No0VmSu087jecH58SFbicbREgZTPJ89mAguupA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"fastify-plugin": "^6.0.0",
"helmet": "^8.0.0"
}
},
"node_modules/@fastify/merge-json-schemas": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
@ -143,6 +242,27 @@
"ipaddr.js": "^2.1.0"
}
},
"node_modules/@fastify/rate-limit": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-11.1.0.tgz",
"integrity": "sha512-BeJ9tizLvmTXGD7deYU5G04OtHhwk5uHxbpEPVp09gKvUBIXmau/4Bshxhu9ci54MvVWfGjCEx4RzvsTntojwA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"fastify-plugin": "^6.0.0",
"toad-cache": "^3.7.0"
}
},
"node_modules/@fastify/send": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz",
@ -167,9 +287,9 @@
}
},
"node_modules/@fastify/static": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz",
"integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==",
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.2.tgz",
"integrity": "sha512-G/g18cG9tLutT/OVyN1AIsHIl9L1UwmJ+S3dkyhVpplIx0nEMicd7RGQ+uJLyhKKF4a3tTcQydccn3Mop1fX+Q==",
"funding": [
{
"type": "github",
@ -183,20 +303,12 @@
"license": "MIT",
"dependencies": {
"@fastify/accept-negotiator": "^2.0.0",
"@fastify/error": "^4.0.0",
"@fastify/send": "^4.0.0",
"content-disposition": "^0.5.4",
"fastify-plugin": "^5.0.0",
"content-disposition": "^2.0.1",
"fastify-plugin": "^6.0.0",
"fastq": "^1.17.1",
"glob": "^11.0.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
"glob": "^13.0.0"
}
},
"node_modules/@lukeed/ms": {
@ -214,6 +326,18 @@
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@ -291,28 +415,79 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
"integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cookie": {
@ -328,19 +503,11 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
@ -360,12 +527,81 @@
"node": ">=6"
}
},
"node_modules/duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.0.0",
"inherits": "^2.0.1",
"readable-stream": "^2.0.0",
"stream-shift": "^1.0.0"
}
},
"node_modules/duplexify/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/duplexify/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/duplexify/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fast-decode-uri-component": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
@ -412,9 +648,9 @@
}
},
"node_modules/fast-uri": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"funding": [
{
"type": "github",
@ -461,9 +697,9 @@
}
},
"node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz",
"integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==",
"funding": [
{
"type": "github",
@ -486,9 +722,9 @@
}
},
"node_modules/find-my-way": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz",
"integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==",
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz",
"integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@ -499,44 +735,33 @@
"node": ">=20"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
"path-scurry": "^2.0.2"
},
"engines": {
"node": ">=14"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"node_modules/helmet": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://github.com/sponsors/EvanHahn"
}
},
"node_modules/http-errors": {
@ -559,6 +784,26 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -574,26 +819,11 @@
"node": ">= 10"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^9.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/json-schema-ref-resolver": {
"version": "3.0.0",
@ -658,9 +888,9 @@
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
@ -678,6 +908,15 @@
"node": ">=10.0.0"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@ -711,19 +950,13 @@
"node": ">=14.0.0"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-scurry": {
@ -742,6 +975,17 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/peek-stream": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
"integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==",
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"duplexify": "^3.5.0",
"through2": "^2.0.3"
}
},
"node_modules/pg": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
@ -907,6 +1151,21 @@
"node": ">=0.10.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
@ -923,12 +1182,75 @@
],
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/pumpify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
"integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
"license": "MIT",
"dependencies": {
"duplexify": "^4.1.1",
"inherits": "^2.0.3",
"pump": "^3.0.0"
}
},
"node_modules/pumpify/node_modules/duplexify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.4.1",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1",
"stream-shift": "^1.0.2"
}
},
"node_modules/pumpify/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
@ -1063,39 +1385,6 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
@ -1123,6 +1412,21 @@
"node": ">= 0.8"
}
},
"node_modules/stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/thread-stream": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
@ -1141,6 +1445,46 @@
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
"license": "MIT"
},
"node_modules/through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"license": "MIT",
"dependencies": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"node_modules/through2/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/through2/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/through2/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/toad-cache": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
@ -1159,20 +1503,17 @@
"node": ">=0.6"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xtend": {
"version": "4.0.2",

View file

@ -9,10 +9,15 @@
},
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
"dev": "node --watch src/server.js",
"test": "node --test"
},
"dependencies": {
"@fastify/static": "^8.0.1",
"@fastify/compress": "^8.3.1",
"@fastify/cookie": "^11.1.1",
"@fastify/helmet": "^13.1.0",
"@fastify/rate-limit": "^11.1.0",
"@fastify/static": "^10.1.2",
"fastify": "^5.1.0",
"pg": "^8.13.1"
}

81
public/css/coloring.css Normal file
View file

@ -0,0 +1,81 @@
/* Thematische kleurplaten
Er staat maar één laag op het papier: de SVG van de plaat. De tekenvolgorde
is de DOM-volgorde (vakken -> verf -> contouren -> kleurwijzer), dus hier
staat geen z-index-stapel en geen mix-blend-mode meer. Belangrijk: dit
bestand zet nooit een `fill` op een kleurvak - dat zou van de vulling van de
verfemmer winnen en de verf onzichtbaar maken. */
.coloring-app{--coloring-ink:#1c293d;--coloring-detail:#2d3e56;height:100%;min-height:0;display:flex;flex-direction:column;background:linear-gradient(180deg,color-mix(in srgb,var(--orange) 4%,var(--surface)),var(--surface));color:var(--ink)}
/* de kleurplaat zelf moet snel te kiezen zijn: thema en plaat blijven dus
altijd zichtbaar, samen met kwast/emmer/gum. Kleurhulp en reset - die je
zelden aanpast - zitten achter het -tandwiel (.edu-settings, zie
teach.css) zodat ze niet permanent ruimte innemen. */
.coloring-tools,.coloring-choices{display:flex;align-items:center;gap:7px;flex-wrap:wrap;padding:9px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));background:color-mix(in srgb,var(--surface) 96%,var(--orange))}
.coloring-choices label{display:grid;grid-template-columns:auto minmax(120px,1fr);align-items:center;gap:6px;color:var(--muted);font-size:11px;font-weight:850}
.coloring-choices select{height:36px;max-width:230px;border:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));border-radius:11px;padding:0 30px 0 10px;background:var(--surface);color:var(--ink);box-shadow:0 2px 8px rgba(30,50,90,.07);font:inherit;font-weight:750}
.coloring-fit{padding:6px 10px;border:1px solid color-mix(in srgb,var(--line) 75%,var(--orange));border-radius:999px;background:color-mix(in srgb,var(--surface) 92%,var(--orange));color:var(--muted);box-shadow:0 2px 8px rgba(30,50,90,.06);font-size:11px;font-weight:800;white-space:nowrap}
.coloring-tool-buttons,.coloring-palette,.coloring-sizes{display:flex;align-items:center;gap:4px;padding:4px;border:1px solid color-mix(in srgb,var(--line) 78%,transparent);border-radius:13px;background:linear-gradient(180deg,color-mix(in srgb,var(--surface-2) 82%,#fff),var(--surface-2));box-shadow:inset 0 1px 0 rgba(255,255,255,.72),0 2px 7px rgba(30,50,90,.06)}
.coloring-tools button{width:36px;height:36px;display:grid;place-items:center;border:0;border-radius:10px;background:transparent;color:var(--ink);font:inherit;font-size:18px;cursor:pointer;transition:transform .14s ease,background .14s ease,box-shadow .14s ease}
.coloring-tool-buttons button{width:auto;min-width:72px;grid-auto-flow:column;grid-template-columns:auto auto;gap:5px;padding:0 9px}
.coloring-tool-icon{display:grid;place-items:center;font-size:18px;line-height:1}.coloring-tool-label{font-size:10px;font-weight:850;line-height:1}
.coloring-tools button:is(:hover,:focus-visible){transform:translateY(-1px);background:var(--surface);box-shadow:0 4px 10px rgba(30,50,90,.14)}
.coloring-tools button.on{background:var(--surface);box-shadow:0 2px 7px rgba(30,50,90,.16)}
.coloring-tool-buttons button.on{outline:2px solid var(--orange);outline-offset:-2px}
.coloring-color i{width:23px;height:23px;display:block;border:1px solid rgba(28,41,61,.25);border-radius:50%;box-shadow:inset 0 2px 3px rgba(255,255,255,.42),inset 0 -2px 3px rgba(28,41,61,.12),0 1px 2px rgba(28,41,61,.12);transition:transform .14s ease}
.coloring-color.on{outline:2.5px solid var(--coloring-ink,#1c293d);outline-offset:-3px}.coloring-color.on i{transform:scale(1.08)}
.coloring-size i{display:block;border-radius:50%;background:var(--ink)}
.coloring-size.on{outline:2px solid var(--orange);outline-offset:-2px}
.coloring-custom{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:10px;font-weight:800}
.coloring-custom input{width:36px;height:36px;padding:3px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 2px 7px rgba(30,50,90,.08);cursor:pointer}
.coloring-guide-btn{width:36px;height:36px;border:0;border-radius:10px;font-size:18px;cursor:pointer;background:color-mix(in srgb,var(--teal) 14%,var(--surface));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--teal) 20%,transparent)}
.coloring-guide-btn.on{outline:2px solid var(--teal);outline-offset:-2px}
.coloring-reset{width:36px;height:36px;border:0;border-radius:10px;font-size:18px;cursor:pointer;background:var(--red-soft);color:var(--red);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--red) 18%,transparent)}
.coloring-tools .coloring-done{width:auto;min-width:112px;margin-left:auto;padding:0 14px;color:#fff;box-shadow:0 4px 10px color-mix(in srgb,var(--orange) 24%,transparent);font-size:13px}
.coloring-stage{flex:1;min-height:0;overflow:auto;display:grid;place-items:center;padding:18px;background:radial-gradient(circle at 12% 18%,color-mix(in srgb,var(--orange) 13%,transparent),transparent 31%),radial-gradient(circle at 88% 82%,color-mix(in srgb,var(--purple) 10%,transparent),transparent 34%),linear-gradient(145deg,var(--surface-2),color-mix(in srgb,var(--purple) 5%,var(--surface)))}
/* het papier vangt alle aanwijzing op; binnen de SVG wordt meetkundig
aangewezen met isPointInFill/isPointInStroke */
.coloring-paper{position:relative;width:min(100%,900px);aspect-ratio:4/3;flex:none;overflow:hidden;border:2px solid rgba(255,255,255,.94);border-radius:22px;background:#fff;box-shadow:inset 0 0 0 1px rgba(28,41,61,.12),0 4px 0 rgba(28,41,61,.05),0 20px 46px rgba(30,50,90,.18);touch-action:none;cursor:crosshair}
/* de plaat draagt haar eigen inktkleuren, zodat een losse SVG (cast, export,
testharnas) er hetzelfde uitziet als in de widget */
.coloring-sheet{--coloring-ink:#1c293d;--coloring-detail:#2d3e56;position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none;shape-rendering:geometricPrecision}
.coloring-sheet [hidden]{display:none}
/* het vak zelf draagt alleen kleur; de contour is een eigen vorm erboven */
.coloring-region{stroke:none}
.coloring-legacy-ink{opacity:.85}
/* verf: één groep per streek, dekking op de groep zodat er binnen een streek
geen donkere naden ontstaan */
.coloring-stroke{pointer-events:none}
.coloring-app[data-tool="eraser"] .coloring-stroke{pointer-events:stroke}
/* contouren: twee lijndiktes, zodat voor- en achtergrond leesbaar blijven */
.coloring-outline{stroke:var(--coloring-ink,#1c293d);stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2}
.coloring-outline.coloring-contour{stroke-width:6.4}
.coloring-outline.coloring-flat{stroke-width:3.4}
.coloring-outline.coloring-backdrop{stroke-width:2.4}
.coloring-detail{stroke:var(--coloring-detail,#2d3e56);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
.coloring-solid{fill:var(--coloring-ink,#1c293d);stroke:none}
.coloring-guide-dot{stroke:#fff;stroke-width:3}
.coloring-hint{min-height:30px;padding:7px 11px;color:var(--muted);font-size:11px;font-weight:700;text-align:center;background:var(--surface)}
.coloring-app[data-tool="brush"] .coloring-paper{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Ctext y='25' font-size='24'%3E%F0%9F%96%8C%EF%B8%8F%3C/text%3E%3C/svg%3E") 5 25,crosshair}
.coloring-app[data-tool="bucket"] .coloring-paper{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Ctext y='24' font-size='23'%3E%F0%9F%AA%A3%3C/text%3E%3C/svg%3E") 6 24,pointer}
.coloring-app[data-tool="eraser"] .coloring-paper{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Ctext y='24' font-size='23'%3E%F0%9F%A7%BD%3C/text%3E%3C/svg%3E") 6 24,pointer}
.widget[data-widget="coloring"]{--widget-accent:var(--orange)}
/* @container in plaats van @media: dit moet reageren op de breedte van het
widget-venster (dat kan smal staan op een brede monitor), niet op de
breedte van de hele browser - zie .widget-body{container-name:widget} */
@container widget (max-width:620px){
.coloring-tools,.coloring-choices{padding:6px;gap:4px}
.coloring-choices label{grid-template-columns:1fr;gap:2px;flex:1;min-width:125px}
.coloring-choices select{width:100%;max-width:none;height:32px}
.coloring-fit{width:100%;text-align:center}
.coloring-tools button,.coloring-guide-btn,.coloring-reset{width:32px;height:32px;font-size:16px}
.coloring-tool-buttons button{min-width:36px;width:36px;padding:0}.coloring-tool-label{display:none}
.coloring-color i{width:18px;height:18px}
.coloring-custom span{display:none}.coloring-custom input{width:32px;height:32px}
.coloring-tools .coloring-done{min-width:90px;margin-left:0;font-size:11px}
.coloring-stage{place-items:start center;padding:8px}
.coloring-paper{min-width:360px;border-radius:10px}
.coloring-hint{font-size:10px}
}
@media(pointer:coarse){.coloring-tools button,.coloring-custom input,.coloring-guide-btn,.coloring-reset{min-width:40px;min-height:40px}.coloring-choices select{min-height:40px}}
@media(prefers-reduced-motion:reduce){.coloring-app *{scroll-behavior:auto!important;transition:none!important}}

View file

@ -0,0 +1,44 @@
.image-catalog-modal{width:min(1080px,96vw);max-height:94vh}
/* titelbalk met ✕ blijft altijd in beeld, ook na scrollen in een lange grid */
.image-catalog-modal .gallery-head{position:sticky;top:-24px;z-index:4;background:var(--surface);padding:4px 0 8px;margin-top:-4px}
.ic-tools{display:grid;grid-template-columns:minmax(180px,1.5fr) repeat(3,minmax(130px,.8fr)) auto;gap:8px;position:sticky;top:12px;z-index:3;background:var(--surface);padding:8px 0 10px}
.ic-tools input,.ic-tools select,.ic-add select,.ic-add input,.ic-editor input,.ic-editor select{min-width:0;width:100%;border:1px solid var(--line);border-radius:10px;padding:9px 10px;background:var(--surface);color:var(--ink);font:inherit}
.ic-word-themes{display:flex;align-items:center;gap:7px;overflow-x:auto;padding:9px 10px;margin:0 0 10px;border:1px solid var(--line);border-radius:13px;background:linear-gradient(135deg,var(--surface-2),var(--accent-soft));scrollbar-width:thin}
.ic-word-themes strong{flex:none;margin-right:3px;font-size:12px;color:var(--muted)}
.ic-word-themes button{flex:none;border:1px solid var(--line);border-radius:999px;padding:7px 11px;background:var(--surface);color:var(--ink);font:inherit;font-size:12px;font-weight:850;cursor:pointer;transition:transform .12s,border-color .12s,background .12s}
.ic-word-themes button:hover{transform:translateY(-1px);border-color:var(--accent)}
.ic-word-themes button.active{border-color:var(--accent);background:var(--accent);color:#fff;box-shadow:var(--shadow-1)}
.ic-quota{padding:8px 12px;margin-bottom:10px;border-radius:10px;background:var(--accent-soft);color:var(--accent-ink);font-size:13px;font-weight:800}
.ic-add{border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-bottom:10px;background:var(--surface-2)}
.ic-add summary{cursor:pointer;font-weight:900}
.ic-add-grid,.ic-editor-grid{display:grid;grid-template-columns:1fr 1fr 1.2fr;gap:10px;margin-top:10px;align-items:end}
.ic-field{display:flex;flex-direction:column;gap:4px;font-size:12px;font-weight:800;color:var(--muted)}
.ic-field select[multiple]{min-height:88px}
.ic-upload{display:flex;align-items:center;justify-content:center;min-height:44px;margin:0;cursor:pointer;text-align:center;color:var(--ink)}
.ic-theme-new{display:flex;gap:8px;margin-top:10px}.ic-theme-new input{flex:1}
.ic-status{min-height:20px;font-size:13px;font-weight:800;color:var(--red);padding:2px 0}.ic-status.ok{color:var(--green)}
.ic-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:10px}
.ic-card{position:relative;min-width:0;border:1px solid var(--line);border-radius:14px;background:var(--surface);overflow:hidden;transition:border-color .15s,box-shadow .15s}
.ic-card:hover{border-color:var(--accent);box-shadow:var(--shadow-1)}
.ic-choose{display:grid;grid-template-rows:120px auto auto;width:100%;height:100%;gap:5px;padding:10px;border:0;background:transparent;color:var(--ink);font:inherit;text-align:left;cursor:pointer}
.ic-choose img{width:100%;height:120px;object-fit:contain;border-radius:9px;background:#fff}
.ic-choose strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ic-choose span{font-size:11px;color:var(--muted);min-height:28px;overflow:hidden}
.ic-card-edit{position:absolute;right:7px;top:7px;width:32px;height:32px;border:1px solid var(--line);border-radius:9px;background:var(--surface);cursor:pointer;box-shadow:var(--shadow-1)}
.ic-editor{position:sticky;bottom:-24px;z-index:4;margin-top:12px;padding:14px;border:2px solid var(--accent);border-radius:14px;background:var(--surface);box-shadow:var(--shadow-2)}
.ic-editor h3{margin:0}.ic-editor-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
.ic-explorer-thumb{width:64px;height:56px;object-fit:contain;border-radius:8px;background:#fff}
.img-quota-admin{display:flex;flex-direction:column;gap:7px}.img-quota-row{display:grid;grid-template-columns:minmax(180px,1fr) 130px 130px auto;align-items:center}
.img-quota-users{display:flex;flex-direction:column;gap:6px;margin-top:8px}
body.hc .ic-card,body.hc .ic-add,body.hc .ic-tools input,body.hc .ic-tools select,body.hc .ic-editor{border-color:#000}
@media(max-width:760px){
/* dvh i.p.v. vh: met een dynamische browserbalk (telefoon) is 100vh hoger
dan het zichtbare scherm, waardoor titel en sluitknop wegvielen */
.image-catalog-modal{width:100vw;max-height:100dvh;height:100dvh;border-radius:0;padding:16px}
.image-catalog-modal .gallery-head{top:-16px}
.ic-tools{top:20px;grid-template-columns:1fr 1fr;padding-top:4px}.ic-search,.ic-folders{grid-column:1/-1}
.ic-word-themes{margin-inline:-2px;padding:8px}.ic-word-themes strong{position:sticky;left:0;z-index:1;padding:7px 6px;background:var(--surface-2)}
.ic-add-grid,.ic-editor-grid{grid-template-columns:1fr}.ic-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
.ic-choose{grid-template-rows:96px auto auto}.ic-choose img{height:96px}
.img-quota-row{grid-template-columns:1fr 1fr}.img-quota-row .am-name{grid-column:1/-1}
}
@media(max-width:420px){.ic-grid{grid-template-columns:1fr}.ic-editor-actions{flex-wrap:wrap}.ic-editor-actions .tbtn{flex:1}}

View file

@ -0,0 +1,89 @@
/* Grafisch live-klassenoverzicht */
#settingsModal .live-class-overview{display:grid;gap:14px;margin-top:10px}
#settingsModal .live-class-overview>.live-session-head{position:sticky;top:0;z-index:3;margin:0;padding:10px 12px;border:1px solid color-mix(in srgb,var(--blue) 30%,var(--line));border-radius:13px;background:color-mix(in srgb,var(--blue) 6%,var(--surface));box-shadow:0 5px 15px rgba(25,45,80,.06)}
#settingsModal .live-class-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
#settingsModal .live-class-metric{display:flex;align-items:center;gap:9px;padding:10px;border:1px solid var(--line);border-radius:12px;background:var(--surface)}
#settingsModal .live-class-metric strong{display:grid;width:35px;height:35px;place-items:center;border-radius:10px;background:var(--surface-2);font-size:18px}
#settingsModal .live-class-metric span{font-size:11px;font-weight:850}
#settingsModal .live-class-metric.active strong{color:var(--green)}
#settingsModal .live-class-metric.new strong{color:var(--blue)}
#settingsModal .live-class-metric.support strong{color:var(--orange)}
#settingsModal .live-class-metric.complete strong{color:var(--accent)}
#settingsModal .live-help-signals{padding:11px;border:1px solid color-mix(in srgb,var(--orange) 45%,var(--line));border-radius:13px;background:color-mix(in srgb,var(--orange) 9%,var(--surface))}
#settingsModal .live-help-signals>strong{display:block;margin-bottom:8px;font-size:12px}
#settingsModal .live-help-signals>div{display:flex;flex-wrap:wrap;gap:6px}
#settingsModal .live-help-chip{border:1px solid color-mix(in srgb,var(--orange) 45%,var(--line));border-radius:999px;padding:5px 9px;background:var(--surface);color:var(--ink);font:inherit;font-size:11px;font-weight:800;cursor:pointer}
#settingsModal .live-help-chip.errors{color:var(--red)}#settingsModal .live-help-chip.idle{color:var(--orange)}
#settingsModal .live-grid-controls{display:flex;align-items:center;justify-content:space-between;gap:10px}
#settingsModal .live-grid-controls .am-sel{width:auto;min-width:150px}
#settingsModal .live-class-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:9px}
#settingsModal .live-pupil-tile{overflow:hidden;border:1px solid var(--line);border-radius:14px;background:var(--surface);box-shadow:inset 4px 0 var(--line)}
#settingsModal .live-pupil-tile.active{border-color:color-mix(in srgb,var(--green) 40%,var(--line));box-shadow:inset 4px 0 var(--green)}
#settingsModal .live-pupil-tile.new{border-color:color-mix(in srgb,var(--blue) 45%,var(--line));box-shadow:inset 4px 0 var(--blue)}
#settingsModal .live-pupil-tile.support{border-color:color-mix(in srgb,var(--orange) 55%,var(--line));box-shadow:inset 4px 0 var(--orange)}
#settingsModal .live-pupil-tile.paused{border-color:color-mix(in srgb,var(--purple) 45%,var(--line));box-shadow:inset 4px 0 var(--purple)}
#settingsModal .live-pupil-tile.complete{box-shadow:inset 4px 0 var(--accent)}
#settingsModal .live-pupil-main{display:grid;width:100%;grid-template-columns:minmax(0,1fr) 76px;gap:5px 8px;padding:10px 10px 8px 14px;border:0;background:transparent;color:var(--ink);font:inherit;text-align:left;cursor:pointer}
#settingsModal .live-pupil-main:hover,#settingsModal .live-pupil-main:focus-visible{background:var(--surface-2);outline:2px solid var(--accent-soft);outline-offset:-2px}
#settingsModal .live-pupil-state{grid-column:1/-1;justify-self:start;padding:2px 7px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:9px;font-weight:900;text-transform:uppercase}
#settingsModal .live-pupil-tile.active .live-pupil-state,#settingsModal .live-pupil-tile.new .live-pupil-state{background:color-mix(in srgb,var(--green) 14%,var(--surface));color:var(--green)}
#settingsModal .live-pupil-tile.support .live-pupil-state{background:color-mix(in srgb,var(--orange) 15%,var(--surface));color:var(--orange)}
#settingsModal .live-pupil-main>strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}
#settingsModal .live-pupil-task{grid-column:1/-1;overflow:hidden;color:var(--muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}
#settingsModal .live-pupil-progress{align-self:center;height:6px;overflow:hidden;border-radius:99px;background:var(--surface-2)}
#settingsModal .live-pupil-progress i{display:block;height:100%;border-radius:inherit;background:var(--accent)}
#settingsModal .live-pupil-main small{grid-column:1;color:var(--muted);font-size:10px}
#settingsModal .live-sparkline{grid-column:2;grid-row:3/6;width:76px;height:28px;align-self:end}
#settingsModal .live-sparkline .line{fill:none;stroke:var(--blue);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
#settingsModal .live-sparkline .empty{stroke:var(--line);stroke-width:2;stroke-dasharray:3 3}
#settingsModal .live-action-toggle{width:100%;padding:6px 10px;border:0;border-top:1px solid var(--line);background:var(--surface-2);color:var(--accent-ink);font:inherit;font-size:10px;font-weight:900;cursor:pointer}
#settingsModal .live-action-row{display:grid;grid-template-columns:1fr 1fr;gap:5px;padding:7px;background:var(--surface-2)}
#settingsModal .live-action-row[hidden]{display:none}
#settingsModal .live-action-row button{min-width:0;padding:6px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--ink);font:inherit;font-size:10px;font-weight:800;cursor:pointer}
#settingsModal .live-action-row button:hover,#settingsModal .live-action-row button:focus-visible{border-color:var(--accent);outline:2px solid var(--accent-soft)}
#settingsModal .live-action-row button:disabled{opacity:.4;cursor:not-allowed}
#settingsModal .live-dashboard-message{min-height:17px;color:var(--accent-ink);font-size:11px;font-weight:800}
#settingsModal .live-graphics-grid{display:grid;grid-template-columns:minmax(230px,.8fr) minmax(330px,1.2fr);gap:10px}
#settingsModal .live-graphic-card{min-width:0;padding:12px;border:1px solid var(--line);border-radius:14px;background:var(--surface)}
#settingsModal .live-graphic-card>strong{display:block;margin-bottom:10px;font-size:13px}
#settingsModal .live-funnel-row{display:grid;grid-template-columns:82px minmax(70px,1fr) 28px;align-items:center;gap:7px;margin:7px 0;font-size:10px}
#settingsModal .live-funnel-track{height:12px;overflow:hidden;border-radius:99px;background:var(--surface-2)}
#settingsModal .live-funnel-track i{display:block;height:100%;border-radius:inherit;background:hsl(calc(198 + var(--funnel-step)*18) 72% 51%)}
#settingsModal .live-funnel-row>strong{text-align:right}
#settingsModal .live-trend svg{display:block;width:100%;height:auto;max-height:190px}
#settingsModal .live-trend .grid{stroke:var(--line);stroke-width:1}.live-trend .axis{fill:var(--muted);font-size:9px}
#settingsModal .live-trend .completion{fill:color-mix(in srgb,var(--purple) 42%,transparent)}
#settingsModal .live-trend .accuracy{fill:none;stroke:var(--blue);stroke-width:4;stroke-linecap:round;stroke-linejoin:round}
#settingsModal .live-trend .accuracy-dot{fill:var(--surface);stroke:var(--blue);stroke-width:3}
#settingsModal .live-chart-legend{display:flex;justify-content:center;gap:15px;color:var(--muted);font-size:10px}
#settingsModal .live-chart-legend span:before{display:inline-block;width:9px;height:9px;margin-right:5px;border-radius:3px;background:var(--blue);content:""}
#settingsModal .live-chart-legend .completed:before{background:color-mix(in srgb,var(--purple) 55%,var(--surface))}
#settingsModal .live-timeline{display:grid;gap:6px}
#settingsModal .live-timeline-row{display:grid;grid-template-columns:minmax(90px,.7fr) minmax(130px,1fr) minmax(80px,1.1fr) 32px;align-items:center;gap:8px;width:100%;padding:7px;border:1px solid var(--line);border-radius:9px;background:var(--surface);color:var(--ink);font:inherit;text-align:left;cursor:pointer}
#settingsModal .live-timeline-row:hover,#settingsModal .live-timeline-row:focus-visible{border-color:var(--accent);outline:2px solid var(--accent-soft)}
#settingsModal .live-timeline-row>strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}
#settingsModal .live-timeline-row>span{overflow:hidden;color:var(--muted);font-size:10px;text-overflow:ellipsis;white-space:nowrap}
#settingsModal .live-timeline-track{height:8px;border-radius:99px;background:var(--surface-2)}
#settingsModal .live-timeline-track i{display:block;height:100%;border-radius:inherit;background:var(--line)}
#settingsModal .live-timeline-row.live .live-timeline-track i{background:var(--green)}
#settingsModal .live-timeline-duration{text-align:right}
/* Leerling: live bericht en echte pauze */
.pupil-live-notice{position:fixed;top:calc(var(--tbar) + 12px);left:50%;z-index:5900;max-width:min(560px,calc(100vw - 24px));transform:translateX(-50%);padding:11px 16px;border:2px solid var(--blue);border-radius:14px;background:var(--surface);box-shadow:var(--shadow-2);color:var(--ink);font-size:14px;font-weight:850;text-align:center}
.pupil-live-notice.encourage{border-color:var(--green)}.pupil-live-notice.easier{border-color:var(--purple)}
.pupil-live-notice[hidden],.pupil-pause-shield[hidden]{display:none}
.pupil-pause-shield{position:fixed;inset:var(--tbar) 0 0;z-index:5800;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:9px;padding:24px;background:color-mix(in srgb,var(--surface) 88%,transparent);backdrop-filter:blur(5px);color:var(--ink);text-align:center}
.pupil-pause-shield>span:first-child{font-size:46px}.pupil-pause-shield strong{font-size:22px}.pupil-pause-shield>span:last-child{max-width:440px;color:var(--muted);font-size:14px}
body.pupil-session-paused #pupilView .widget-body{pointer-events:none;user-select:none;filter:saturate(.45)}
@media(max-width:760px){
#settingsModal .live-class-metrics{grid-template-columns:1fr 1fr}
#settingsModal .live-graphics-grid{grid-template-columns:1fr}
#settingsModal .live-timeline-row{grid-template-columns:1fr 1fr}
#settingsModal .live-timeline-track{grid-column:1/-1}
}
@media(max-width:480px){
#settingsModal .live-class-grid{grid-template-columns:1fr}
#settingsModal .live-grid-controls{align-items:stretch;flex-direction:column}
#settingsModal .live-grid-controls .am-sel{width:100%}
}

2628
public/css/teach.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
Quicksand - ontworpen door Andrew Paglinawan e.a.
Gelicenseerd onder de SIL Open Font License 1.1 (https://openfontlicense.org).
Bron: Google Fonts (https://fonts.google.com/specimen/Quicksand).
Dit bestand (quicksand.woff2) is het latin-subset van de variabele font (gewichten 300-700).

Binary file not shown.

48
public/img/logo.svg Normal file
View file

@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 104" role="img" aria-label="teachus.today uil">
<!-- laag-poly uil: vlakke facetten in het merk-palet, transparante achtergrond -->
<!-- oren -->
<polygon points="33,28 40,10 48,22" fill="#1d3461"/>
<polygon points="62,22 70,10 77,28" fill="#1d3461"/>
<!-- voorhoofd -->
<polygon points="40,10 70,10 62,22 48,22" fill="#2456b8"/>
<!-- gezicht -->
<polygon points="33,28 48,22 44,38" fill="#2b4d94"/>
<polygon points="77,28 62,22 66,38" fill="#2b4d94"/>
<polygon points="48,22 62,22 66,38 44,38" fill="#24407a"/>
<!-- wangen -->
<polygon points="33,28 44,38 36,46" fill="#2f6fe4"/>
<polygon points="77,28 66,38 74,46" fill="#2f6fe4"/>
<!-- ogen -->
<circle cx="48.5" cy="30.5" r="4.6" fill="#cfe0f5"/>
<circle cx="61.5" cy="30.5" r="4.6" fill="#cfe0f5"/>
<circle cx="48.5" cy="30.5" r="2.3" fill="#101d3a"/>
<circle cx="61.5" cy="30.5" r="2.3" fill="#101d3a"/>
<!-- snavel -->
<polygon points="55,36 58.5,41 55,46 51.5,41" fill="#f28c38"/>
<!-- linkervleugel: cyaan/teal -->
<polygon points="36,46 44,38 44,56" fill="#3ac2e8"/>
<polygon points="36,46 44,56 32,62" fill="#18c5c0"/>
<polygon points="32,62 44,56 40,74" fill="#66d4ef"/>
<polygon points="32,62 40,74 30,78" fill="#12a8a4"/>
<polygon points="30,78 40,74 38,88" fill="#18c5c0"/>
<!-- borst: roze/oranje -->
<polygon points="44,38 55,46 44,56" fill="#e83e8c"/>
<polygon points="55,46 66,38 66,56" fill="#f28c38"/>
<polygon points="44,56 55,46 58,64" fill="#c2257f"/>
<polygon points="55,46 66,56 58,64" fill="#f06ba8"/>
<polygon points="44,56 58,64 48,78" fill="#f5a55c"/>
<polygon points="58,64 66,56 68,74" fill="#e83e8c"/>
<polygon points="48,78 58,64 68,74" fill="#8b5cf6"/>
<polygon points="48,78 68,74 58,86" fill="#f28c38"/>
<!-- rechterflank: blauw/navy -->
<polygon points="66,38 74,46 66,56" fill="#2f6fe4"/>
<polygon points="66,56 74,46 76,66" fill="#1d3461"/>
<polygon points="66,56 76,66 68,74" fill="#2456b8"/>
<polygon points="68,74 76,66 70,84" fill="#1d3461"/>
<!-- tak -->
<polygon points="26,92 106,85 106,88.4 26,95.4" fill="#1d3461"/>
<!-- blaadjes -->
<polygon points="92,70 99,78 92,84 85,78" fill="#18c5c0"/>
<polygon points="104,77 110,83 104,88 98,83" fill="#3ac2e8"/>
<polygon points="96,90 102,95 96,100 90,95" fill="#12a8a4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,8 @@
# Bronnen euro-afbeeldingen
Deze afbeeldingen worden lokaal meegeleverd, zodat de geldwidget ook zonder internetverbinding werkt.
- **Munten (1, 2, 5, 10, 20 en 50 cent, €1 en €2):** actuele gemeenschappelijke zijden van de Europese Commissie, gedownload van [Common sides of euro coins](https://economy-finance.ec.europa.eu/euro/euro-coins-and-notes/euro-coins/common-sides-euro-coins_en) op 15 juli 2026. De tweedimensionale afbeeldingen zijn eigendom van de Europese Unie en worden hier educatief gebruikt volgens de reproductievoorwaarden van de Europese Commissie.
- **Bankbiljetten (€5, €10, €20 en €50):** voorzijde van de Europa-serie met Lagarde-handtekening uit het officiële [ECB-pakket met 72-dpi SPECIMEN-afbeeldingen](https://www.ecb.europa.eu/euro/shared/img/euro_banknotes_specimen_72dpi.zip), gedownload op 15 juli 2026. De zichtbare SPECIMEN-markering en originele 72-dpi-bestanden worden behouden volgens de [ECB-reproductieregels](https://www.ecb.europa.eu/euro/banknotes/images/html/index.en.html).
De bestandsnamen gebruiken centen als waarde, bijvoorbeeld coin-200.png voor €2 en note-500.jpg voor €5.

BIN
public/img/money/coin-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/img/money/coin-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/img/money/coin-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

View file

@ -0,0 +1,115 @@
# Wereldoriëntatie beeldbronnen
De lokale WebP-bestanden zijn uitsluitend verkleinde en, waar nodig, gekaderde
afgeleiden van de hieronder genoemde foto's. De soortnamen zijn gecontroleerd
aan de hand van de bestandsbeschrijvingen op Wikimedia Commons.
## Bomen
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `trees/oak.webp` | [1. English Oak (Quercus robur)](https://commons.wikimedia.org/wiki/File:1._English_Oak_(Quercus_robur)_(3607652442).jpg) | AnemoneProjectors | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `trees/birch.webp` | [Betula pendula winter](https://commons.wikimedia.org/wiki/File:Betula_pendula_winter.jpg) | Maplan | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/horse-chestnut.webp` | [Paardenkastanje2](https://commons.wikimedia.org/wiki/File:Paardenkastanje2.jpg) | Toubib | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/pine.webp` | [Pinus sylvestris Glenmuick](https://commons.wikimedia.org/wiki/File:Pinus_sylvestris_Glenmuick.jpg) | Hello, I am Bruce | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `trees/beech.webp` | [Fagales Fagus sylvatica](https://commons.wikimedia.org/wiki/File:Fagales_-_Fagus_sylvatica.jpg) | Emőke Dénes | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/maple.webp` | [Acer-pseudoplatanus](https://commons.wikimedia.org/wiki/File:Acer-pseudoplatanus.jpg) | MPF | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/willow.webp` | [Salix alba an der Isar](https://commons.wikimedia.org/wiki/File:Salix_alba_an_der_Isar.jpg) | Burkhard Mücke | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/alder.webp` | [Alnus glutinosa 011](https://commons.wikimedia.org/wiki/File:Alnus_glutinosa_011.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `trees/lime.webp` | [Winterlinde (Wiki Loves Earth 2015)](https://commons.wikimedia.org/wiki/File:012_2015_05_06_Winterlinde_(Wiki_Loves_Earth_2015).jpg) | Friedrich Haag | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/ash.webp` | [Fraxinus excelsior 001](https://commons.wikimedia.org/wiki/File:Fraxinus_excelsior_001.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `trees/sweet-chestnut.webp` | [Anatolian sweet chestnut](https://commons.wikimedia.org/wiki/File:Castanea_sativa_-_Anatolian_sweet_chestnut_04.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/rowan.webp` | [Rowan tree 20081002b](https://commons.wikimedia.org/wiki/File:Rowan_tree_20081002b.jpg) | Eeno11 | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/hawthorn.webp` | [Crataegus monogyna in Joncret](https://commons.wikimedia.org/wiki/File:Crataegus_monogyna_in_Joncret_AR1aJPG.jpg) | Jean-Pol GRANDMONT | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `trees/aspen.webp` | [Populus tremula 004](https://commons.wikimedia.org/wiki/File:Populus_tremula_004.jpg) | Willow | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
| `trees/spruce.webp` | [Kuusk Keila-Paldiski rdt ääres](https://commons.wikimedia.org/wiki/File:Kuusk_Keila-Paldiski_rdt_%C3%A4%C3%A4res.jpg) | Ivar Leidus | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/larch.webp` | [European larch Larix decidua](https://commons.wikimedia.org/wiki/File:European_larch_-_Larix_decidua.jpg) | MrPanyGoff | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/yew.webp` | [Witchs Yew Tree](https://commons.wikimedia.org/wiki/File:2020_Witch%E2%80%99s_Yew_Tree_IRL.jpg) | Blarney Castle Estate | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/elm.webp` | [Ulmus minor tree](https://commons.wikimedia.org/wiki/File:Ulmus_minor_tree.jpg) | Ptelea | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
## Bladeren en naalden
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `leaves/oak.webp` | [Zomereik blad Quercus robur](https://commons.wikimedia.org/wiki/File:Zomereik_blad_Quercus_robur.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `leaves/birch.webp` | [Atlas roslin pl Brzoza brodawkowata 1393 6470](https://commons.wikimedia.org/wiki/File:Atlas_roslin_pl_Brzoza_brodawkowata_1393_6470.jpg) | Joanna Boisse | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/horse-chestnut.webp` | [Aesculus hippocastanum in botanical garden of UKW Bydgoszcz (2)](https://commons.wikimedia.org/wiki/File:Aesculus_hippocastanum_in_botanical_garden_of_UKW_Bydgoszcz_(2).jpg) | Tournasol7 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `leaves/pine.webp` | [20151215Pinus sylvestris2](https://commons.wikimedia.org/wiki/File:20151215Pinus_sylvestris2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/beech.webp` | [20130530Hain Rotbuche](https://commons.wikimedia.org/wiki/File:20130530Hain_Rotbuche.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/maple.webp` | [Acer pseudoplatanus 003](https://commons.wikimedia.org/wiki/File:Acer_pseudoplatanus_003.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `leaves/willow.webp` | [20160721Salix alba1](https://commons.wikimedia.org/wiki/File:20160721Salix_alba1.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/alder.webp` | [20200531Alnus glutinosa2](https://commons.wikimedia.org/wiki/File:20200531Alnus_glutinosa2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/lime.webp` | [Herbarium Tilia cordata img-007](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Tilia_cordata._img-007.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/ash.webp` | [Herbarium Fraxinus img-015](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Fraxinus._img-015.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/sweet-chestnut.webp` | [Castanea sativa leaves and immature fruit](https://commons.wikimedia.org/wiki/File:Castanea_sativa_leaves_and_immature_fruit.jpg) | Ekir | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `leaves/rowan.webp` | [Herbarium Sorbus aucuparia img-005](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Sorbus_aucuparia._img-005.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/hawthorn.webp` | [Crataegus monogyna leaves and stipules](https://commons.wikimedia.org/wiki/File:(MHNT)_Crataegus_monogyna_-_Leaves_and_stipules.jpg) | Didier Descouens | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/aspen.webp` | [Herbarium Populus tremula img-017](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Populus_tremula._img-017.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/spruce.webp` | [Picea abies needles in Väsmestorp](https://commons.wikimedia.org/wiki/File:Picea_abies_needles_in_V%C3%A4smestorp_2024-10-29_(0261).jpg) | Achird | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/larch.webp` | [Larix decidua needles and male cones](https://commons.wikimedia.org/wiki/File:Larix_decidua_needles_and_male_cones.JPG) | Rosser1954 / Roger Griffith | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
| `leaves/yew.webp` | [Yew berries](https://commons.wikimedia.org/wiki/File:Yew_berries_(50578142398).jpg) | hedera.baltica | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `leaves/elm.webp` | [Elm leaves (Ulmus minor)](https://commons.wikimedia.org/wiki/File:Elm_leaves_(Ulmus_minor).jpg) | Daniel Capilla | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
## Vogels
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `birds/robin.webp` | [Erithacus rubecula, Roodborstje](https://commons.wikimedia.org/wiki/File:Erithacus_rubecula,_Roodborstje.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/great-tit.webp` | [Parus major m](https://commons.wikimedia.org/wiki/File:Parus_major_m.jpg) | Sławek Staszczuk | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/blackbird.webp` | [Solsort](https://commons.wikimedia.org/wiki/File:Solsort.jpg) | Malene Thyssen | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/mallard.webp` | [Duisburg 10401IMG 0401](https://commons.wikimedia.org/wiki/File:Duisburg_10401IMG_0401.JPG) | Mbdortmund | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) |
| `birds/blue-tit.webp` | [Cyanistes caeruleus 3 Luc Viatour](https://commons.wikimedia.org/wiki/File:Cyanistes_caeruleus_3_Luc_Viatour.jpg) | Luc Viatour | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/house-sparrow.webp` | [Huismus, man](https://commons.wikimedia.org/wiki/File:Huismus,_man.jpg) | JrPol | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/magpie.webp` | [Сорока (Pica pica), Калининград](https://commons.wikimedia.org/wiki/File:%D0%A1%D0%BE%D1%80%D0%BE%D0%BA%D0%B0_(Pica_pica),_%D0%9A%D0%B0%D0%BB%D0%B8%D0%BD%D0%B8%D0%BD%D0%B3%D1%80%D0%B0%D0%B4.jpg) | Dion Art | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/woodpecker.webp` | [Grote bonte specht](https://commons.wikimedia.org/wiki/File:Grote_bonte_specht.JPG) | Usitea | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/chaffinch.webp` | [Fringilla coelebs chaffinch male](https://commons.wikimedia.org/wiki/File:Fringilla_coelebs_chaffinch_male_edit2.jpg) | MichaelMaggs / Arad | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
| `birds/starling.webp` | [Sturnus vulgaris England standing](https://commons.wikimedia.org/wiki/File:Sturnus_vulgaris_-England_-standing-8.jpg) | Mark Robinson | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) |
| `birds/grey-heron.webp` | [Grey heron 2022 04 03 03](https://commons.wikimedia.org/wiki/File:Grey_heron_2022_04_03_03.jpg) | Alexis Lours | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `birds/mute-swan.webp` | [Wild Mute swan at Lake Geneva](https://commons.wikimedia.org/wiki/File:093_Wild_Mute_swan_at_Lake_Geneva_during_sunset_Photo_by_Giles_Laurent.jpg) | Giles Laurent | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/coot.webp` | [Fulica atra Common coot](https://commons.wikimedia.org/wiki/File:Fulica_atra_-_Common_coot,_Adana_2016-11-12_01-1.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/greylag-goose.webp` | [Graugans Anser Anser](https://commons.wikimedia.org/wiki/File:Graugans_Anser_Anser.jpg) | H005 / Pro2 | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
| `birds/collared-dove.webp` | [Eurasian Collared Dove](https://commons.wikimedia.org/wiki/File:Streptopelia_decaocto_-_Eurasian_Collared_Dove_05.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/wren.webp` | [Eurasian wren](https://commons.wikimedia.org/wiki/File:Eurasian_wren_(Troglodytes_troglodytes).jpg) | Alexis Lours | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `birds/buzzard.webp` | [Buteo buteo juv Log](https://commons.wikimedia.org/wiki/File:Buteo_buteo_juv_Log_(2).jpg) | Yerpo | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/barn-owl.webp` | [Tyto alba 1 Luc Viatour](https://commons.wikimedia.org/wiki/File:Tyto_alba_1_Luc_Viatour.jpg) | Luc Viatour | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
## Wilde planten
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `plants/dandelion.webp` | [Paardenbloem (Taraxacum officinale)](https://commons.wikimedia.org/wiki/File:Paardenbloem_(Taraxacum_officinale)_06.JPG) | Dominicus Johannes Bergsma | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `plants/nettle.webp` | [Brandnetel, Urtica dioica](https://commons.wikimedia.org/wiki/File:Brandnetel,_Urtica_dioica,_Locatie,_Famberhorst.jpg) | Dominicus Johannes Bergsma | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/daisy.webp` | [Bellis perennis full plant](https://commons.wikimedia.org/wiki/File:Bellis_perennis_full_plant.jpg) | Grand-Duc | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `plants/red-clover.webp` | [Trifolium pratense](https://commons.wikimedia.org/wiki/File:Trifolium_pratense_RF.jpg) | Robert Flogaus-Faust | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `plants/white-deadnettle.webp` | [Lamium album](https://commons.wikimedia.org/wiki/File:Lamium_album_RF.jpg) | Robert Flogaus-Faust | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `plants/cow-parsley.webp` | [Fluitenkruid (Anthriscus sylvestris)](https://commons.wikimedia.org/wiki/File:Fluitenkruid_(Anthriscus_sylvestris)_26-04-2020._(actm.)_02.jpg) | Agnes Monkelbaan | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/yarrow.webp` | [Achillea millefolium](https://commons.wikimedia.org/wiki/File:Achillea_millefolium_(52354).jpg) | Rhododendrites | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/ribwort-plantain.webp` | [Plantago lanceolata kz09](https://commons.wikimedia.org/wiki/File:Plantago_lanceolata_kz09.jpg) | Krzysztof Ziarnek, Kenraiz | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/common-poppy.webp` | [Papaver rhoeas LC0050](https://commons.wikimedia.org/wiki/File:Papaver_rhoeas_LC0050.jpg) | Jörg Hempel | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
| `plants/chicory.webp` | [Cichorium intybus](https://commons.wikimedia.org/wiki/File:Cichorium_intybus_TK_2021-07-12_1.jpg) | T. Kebert | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/foxglove.webp` | [Digitalis purpurea LC0101](https://commons.wikimedia.org/wiki/File:Digitalis_purpurea_LC0101.jpg) | Jörg Hempel | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `plants/common-sorrel.webp` | [Rumex acetosa in Aveyron](https://commons.wikimedia.org/wiki/File:Rumex_acetosa_in_Aveyron_(7).jpg) | Tournasol7 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
## Topografie
`netherlands-provinces.svg` is een vereenvoudigde afgeleide zonder labels van
[Netherlands provinces map blank.svg](https://commons.wikimedia.org/wiki/File:Netherlands_provinces_map_blank.svg)
door Gust Justice, gelicentieerd onder
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). De afgeleide
provinciecontouren worden onder dezelfde licentie verspreid.
De plaatsing van stadspunten is daarnaast gecontroleerd met
[Map provinces Netherlands-en.svg](https://commons.wikimedia.org/wiki/File:Map_provinces_Netherlands-en.svg)
door Alphathon ([CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/)).
`world-continents.svg` is gegenereerd uit de fysieke
[Natural Earth 1:110m Geography Regions Polygons](https://www.naturalearthdata.com/downloads/110m-physical-vectors/110m-physical-labels/)
en, voor de eilanden van Oceanië,
[Admin 0 Countries](https://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/).
Natural Earth stelt deze geografische brondata beschikbaar als publiek domein.
De lokale afgeleide bevat zeven afzonderlijk bedienbare werelddeelcontouren.
De vier Nederlandse landsdelen volgen de
[CBS-indeling voor Gebieden in Nederland 2026](https://www.cbs.nl/nl-nl/cijfers/detail/86247NED).
De selectie en loop van de grote rivieren zijn gecontroleerd aan de hand van het
[Rijkswaterstaat-overzicht Rivieren](https://www.rijkswaterstaat.nl/water/waterbeheer/beheer-en-ontwikkeling-rijkswateren/rivieren).

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Some files were not shown because too many files have changed in this diff Show more