<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Gerwin Kuijntjes | Blog &amp; Projecten</title><description>Persoonlijke website van Gerwin Kuijntjes - Blogposts en projecten</description><link>https://gerwinkuijntjes.nl/</link><language>nl</language><item><title>AI is een auto, geen fiets: zo gebruik je het echt als developer</title><link>https://gerwinkuijntjes.nl/nl/blog/ai-is-een-auto-geen-fiets/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/blog/ai-is-een-auto-geen-fiets/</guid><description>Waarom programmeren met AI een vaardigheid is en geen bedreiging, en hoe het zien als een auto in plaats van een fiets je veel effectiever maakt</description><pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Wie de code geschreven heeft, boeit me allang niet meer. Wat telt, is of het werkt en of het deugt, en dat staat los van wie er achter het toetsenbord zat. Of ik nu elk karakter zelf intik of een AI de code genereert op basis van een prompt die ik bedacht heb, maakt niets uit. Het auteurschap is bijzaak. Het draait om het resultaat.&lt;/p&gt;
&lt;p&gt;De verantwoordelijkheid blijft wel hetzelfde. Levert de AI iets op dat stuk is, dan is dat mijn schuld. Ik heb hem zijn gang laten gaan, ik heb zijn werk beoordeeld en ik heb mijn naam onder de commit gezet. Daar heb ik nooit moeite mee gehad. Het werkt net zoals met een junior developer in je team: je geeft het werk uit handen, maar je blijft zelf verantwoordelijk voor wat er uiteindelijk staat. Zodra dat eenmaal echt tot je doordringt, voelt AI niet meer als een vreemde die door je codebase loopt te dwalen, maar gewoon als een vanzelfsprekend onderdeel van hoe je dingen bouwt.&lt;/p&gt;
&lt;h2&gt;Ik zat ook in de ontkenningsfase&lt;/h2&gt;
&lt;p&gt;Veel developers zijn er stiekem nog van overtuigd dat AI speelgoed is, of een bedreiging, of allebei tegelijk. Ik ga ze niet de les lezen, want ik heb in precies hetzelfde schuitje gezeten. Ik bleef sceptisch tot GPT-4o in mei 2024 verscheen en ik het iets zag doen waarvan ik dacht dat het nog jaren weg lag. Toen brak de weerstand en nam de nieuwsgierigheid het over.&lt;/p&gt;
&lt;p&gt;De klacht die ik het vaakst hoor, is een variant op &quot;ik heb het geprobeerd en de code was slecht.&quot; Soms klopt dat. Maar veel vaker zit de oorzaak in de instructie zelf. Het model de schuld geven van een vage prompt is hetzelfde als een nieuwe collega afrekenen op een taak die je nooit fatsoenlijk hebt uitgelegd.&lt;/p&gt;
&lt;h2&gt;Prompten is een managementvaardigheid&lt;/h2&gt;
&lt;p&gt;Dit is het belangrijkste punt. Goed code kunnen schrijven en goed werk uit een AI weten te halen zijn twee verschillende vaardigheden, en wie het eerste beheerst heeft daarmee nog geen enkele garantie op het tweede.&lt;/p&gt;
&lt;p&gt;Denk aan wat er gebeurt als een briljante engineer voor het eerst manager wordt. Al dat moeizaam opgebouwde technische talent telt opeens minder, want het werk is niet meer om het probleem zelf op te lossen. Het werk is nu om het probleem helder te omschrijven, het over te dragen, de kaders te stellen en erop te vertrouwen dat een ander het verder brengt. Sommige van de beste programmeurs worden waardeloze managers, juist omdat ze hun handen niet van het toetsenbord kunnen houden.&lt;/p&gt;
&lt;p&gt;Prompten is dezelfde managementvaardigheid in een nieuwe gedaante. Je legt uit welk resultaat je wil, je benoemt de regels waar het werk zich aan moet houden, en je laat de agent het zware werk doen. Developers die AI wegwuiven leggen het vaak langs de enige meetlat die ze al kennen, namelijk code schrijven, en zien daardoor over het hoofd dat de echte winst in een totaal andere vaardigheid zit: iemand anders gericht naar een resultaat sturen, iets wat de meesten nooit bewust hebben geoefend.&lt;/p&gt;
&lt;h2&gt;Behandel het als een junior developer&lt;/h2&gt;
&lt;p&gt;Het mentale model dat mij het meest geholpen heeft, is om de AI precies te behandelen als een veelbelovende junior.&lt;/p&gt;
&lt;p&gt;Geef je een goede junior een taak, dan dicteer je niet elke toetsaanslag. Je vertelt wat succes is, je geeft hem de context die hij echt nodig heeft en niet meer, en je laat bewust ruimte over om sommige dingen zelf uit te zoeken, want juist in dat geworstel ontstaan de goede beslissingen. Je beschrijft de bestemming en vertrouwt erop dat hij een verstandige route kiest.&lt;/p&gt;
&lt;p&gt;Een goede junior stelt ook vragen. Is de taak onduidelijk of spreken de eisen elkaar tegen, dan wil je dat hij terugkomt en het navraagt, in plaats van twee dagen stilletjes het verkeerde te bouwen. Met AI gaat het net zo, en je kunt om hetzelfde gedrag vragen. Zeg dat hij alles wat onduidelijk is eerst moet uitvragen voordat hij begint, en hij legt de gaten in je eigen denken bloot waarvan je niet wist dat ze er zaten. Sommige van mijn beste prompts waren juist die waarbij de agent me drie scherpe vragen stelde en ik besefte dat ik nog helemaal niet had bedacht wat ik eigenlijk wilde.&lt;/p&gt;
&lt;p&gt;Verder reageert AI op dezelfde aanpak als een junior. Beschrijf het resultaat dat je wil en, heel belangrijk, de reden erachter. Dat &quot;waarom&quot; doet meer dan je zou denken, want zodra de agent je bedoeling snapt, kan hij verstandige keuzes maken in al die kleine situaties die je vergeten bent te noemen. Laat het &quot;waarom&quot; weg en hij vliegt blind zodra de werkelijkheid afwijkt van wat jij voor ogen had.&lt;/p&gt;
&lt;p&gt;En net als elke junior wordt hij dramatisch beter zodra hij zijn eigen werk kan controleren. Geef hem de middelen om te checken wat hij gebouwd heeft: een testsuite die hij kan draaien, een browser die hij kan openen, een screenshot die hij kan bekijken, een type checker die hem betrapt op het moment dat hij zichzelf voor de gek houdt. Zonder feedbackloop zit een AI te gokken en te hopen. Mét feedbackloop vangt hij zijn eigen fouten op voordat ze jou bereiken, en gaat de kwaliteit met sprongen omhoog.&lt;/p&gt;
&lt;h2&gt;De fiets en de auto&lt;/h2&gt;
&lt;p&gt;Dit is de analogie waar ik steeds op terugkom, en het is vast geen toeval dat ik juist bij deze uitkwam. We zijn hier nu eenmaal een fietsland in hart en nieren. We leren als klein kind fietsen, er zijn meer fietsen dan inwoners, en voor de meeste korte ritjes denkt niemand aan de auto. Dus toen ik zocht naar een beeld voor een gereedschap dat je veel verder en veel sneller brengt, kwam vanzelf het contrast boven dat iedere Nederlander in zijn benen voelt: de fiets waar je op opgroeide, en de auto waar je naar grijpt zodra de rit lang wordt.&lt;/p&gt;
&lt;p&gt;Een menselijke coder is een fiets. Elke meter komt uit je eigen benen, en dat is nou juist wat een fiets zo goed maakt. Je voelt de weg door je stuur, je wringt je door een steegje waar geen auto ooit doorheen komt, je staat binnen een halve meter stil, en je rijdt tot aan de voordeur waar geen weg naartoe leidt. Die nabijheid en precisie zijn het hele punt. Maar er hangt een prijskaartje aan. Een fiets is traag over afstand, hij put je uit, en hem honderd kilometer open snelweg laten afleggen is gewoon het verkeerde gereedschap voor de klus. Duw een fiets te ver door en juist wat hem zo wendbaar maakte, wordt zijn beperking.&lt;/p&gt;
&lt;p&gt;AI is een auto. De kracht komt niet meer uit je benen, dus opeens is afstand goedkoop en verdwijnt het brede terrein dat je op de fiets uitputte moeiteloos onder de wielen. Dat is het cadeau: enorm bereik voor heel weinig eigen inspanning. Maar een auto vraagt om dingen die een fiets nooit nodig had. Hij heeft wegen nodig, en in onze wereld zijn dat je specs en je architectuur. Hij heeft vangrails nodig, en dat zijn je tests en je reviews. Haal die infrastructuur weg en een snelle auto wordt gevaarlijk, want snelheid zonder weg betekent alleen dat je eerder in de sloot belandt. En bij al zijn bereik is een auto lomp waar een fiets sierlijk is. Er zijn smalle paadjes waar hij simpelweg niet op kan, plekken waar de juiste zet is om hem te parkeren en het laatste stuk zelf te trappen.&lt;/p&gt;
&lt;p&gt;De echte vaardigheid is dus het terrein lezen. De auto een voetpad op sturen waar alleen een fiets past is dom, en uit pure koppigheid een lange kaarsrechte snelweg op de fiets afzwoegen net zo goed. Het zijn geen concurrenten. Ze bestrijken verschillend terrein, en een developer die tussen die twee kan schakelen komt op plekken die geen van beide alleen ooit zou bereiken.&lt;/p&gt;
&lt;p&gt;Er is nog één verschil, en dat is het verschil dat mij overtuigde om voor de grote ritten op de auto te leunen. Op de fiets is een verkeerde afslag duur. Vijfentwintig kilometer de verkeerde kant op kost je zo je halve middag, want elke kilometer terug betaal je in zweet. In de auto merk je diezelfde fout amper. Je ziet het, je draait om, en een paar minuten later zit je weer op koers alsof er niets gebeurd is. AI werkt precies zo. Soms slaat hij een verkeerde afslag in of bouwt hij iets wat je niet gevraagd had, maar corrigeren gaat snel en kost bijna niets. Je wijst hem terug naar de bestemming en je rijdt door. Juist door die scheve verhouding, waarbij een misser met de auto bijna niets kost om te herstellen, kun je hem vertrouwen bij de grote, ambitieuze klussen, ook al maakt hij af en toe een fout.&lt;/p&gt;
&lt;p&gt;En bij dit alles blijf jij de bestuurder. De auto heeft enorme kracht en geen flauw idee waar jij naartoe wil. Jij kiest de bestemming, jij houdt het stuur vast, jij let op de weg. Zet je de fiets aan de kant, dan verhuist het vakmanschap simpelweg mee naar de bestuurdersstoel.&lt;/p&gt;
&lt;h2&gt;Verdient iemand hier eigenlijk geld mee?&lt;/h2&gt;
&lt;p&gt;Het laatste bezwaar komt meestal vermomd als de doorslaggevende klap: &quot;die AI-bedrijven maken niet eens winst, dus de hele boel stort straks gewoon in.&quot; Het is goed om hier nauwkeurig te zijn, want dat beeld begint te kantelen. Halverwege 2026 rapporteerde Anthropic zijn eerste kwartaal met operationele winst, ergens rond de 559 miljoen dollar, gedragen door werkelijk explosieve omzetgroei.&lt;/p&gt;
&lt;p&gt;Ik wil eerlijk zijn over wat dat wel en niet bewijst. Het gaat om één kwartaal operationele winst, niet om duurzame, bedrijfsbrede nettowinst, en Anthropic heeft zelf gezegd dat het misschien niet het hele jaar in de zwarte cijfers blijft, omdat het flink uitgeeft om te blijven groeien. Een ererondje is dit dus niet. Maar een trendlijn blijft een trendlijn, en deze buigt onmiskenbaar één kant op. Erop wedden dat deze technologie stilletjes verdwijnt is op dit moment een ronduit slechte gok. De verstandige zet is rekenen op het blijven, want blijven gaat het. En zelfs als de allerbeste cloudmodellen zichzelf morgen op de een of andere manier onbetaalbaar zouden maken: lokale LLM&apos;s worden elke maand beter en goedkoper om te draaien. Deze technologie gaat niet meer terug in het doosje.&lt;/p&gt;
&lt;h2&gt;Stap in de auto&lt;/h2&gt;
&lt;p&gt;Dit is dus mijn duwtje in de rug, van de ene developer aan de andere. Kom uit die ontkenningsfase. Je hoeft je vakmanschap er niet voor op te geven, en dat moet je ook vooral niet doen. Blijf kritisch, blijf om kwaliteit geven, en hou je fiets voor de straatjes waar niets eraan kan tippen.&lt;/p&gt;
&lt;p&gt;Stap alleen ook eens in die auto. En leer er dan in rijden: leer een bestemming uitleggen, leer de vangrails neerzetten, en laat de motor het werk doen terwijl jij aan het stuur zit. Doe dat, en je komt verder en sneller dan je op de fiets ooit zou komen.&lt;/p&gt;</content:encoded><category>AI</category><category>Software Development</category></item><item><title>Geldhoek: privacyvriendelijke rekentools voor je geldvragen</title><link>https://gerwinkuijntjes.nl/nl/projecten/geldhoek-nederlandse-rekentools-voor-je-geld/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/projecten/geldhoek-nederlandse-rekentools-voor-je-geld/</guid><description>Een gratis, privacyvriendelijke verzameling Nederlandse rekentools op één plek: netto salaris, maximale hypotheek, toeslagen, sparen en box 3. Alle berekeningen draaien client-side, met privacyvriendelijke analytics die alleen statistieken tonen en nooit je invoer, gebouwd op één bron van waarheid voor de fiscale cijfers die elk jaar tegen officiële bronnen wordt gecontroleerd.</description><pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nederlandse geldvragen zijn bedrieglijk lastig te beantwoorden. Hoeveel houd ik echt over van een salaris van € 45.000? Hoeveel hypotheek kan ik krijgen? Is die loonsverhoging het waard na belasting en gemiste toeslagen? Elke vraag klinkt simpel, maar de regels erachter zijn versnipperd over de Belastingdienst, het Nibud, Dienst Toeslagen en NHG, en ze beïnvloeden elkaar. Een hoger salaris verhoogt je netto inkomen, maar verkleint tegelijk je zorgtoeslag en verandert hoeveel je kunt lenen. Niets daarvan zie je terug in één losse calculator.&lt;/p&gt;
&lt;p&gt;Ik liep hier zelf telkens tegenaan, en elke keer dat ik online een antwoord zocht stuitte ik op dezelfde muren. De goede calculators waren losse, op zichzelf staande tools. Veel ervan zaten vol advertenties, zaten achter een leadformulier, of trackten stilletjes alles wat ik over mijn eigen financiën intikte. En vaak waren de cijfers een jaar of twee verouderd, en voor belastingschijven en toeslaggrenzen betekent dat simpelweg een fout antwoord. Daarom heb ik &lt;a href=&quot;https://geldhoek.nl&quot;&gt;Geldhoek&lt;/a&gt; gebouwd.&lt;/p&gt;
&lt;h2&gt;Het probleem: versnipperde regels, versnipperde tools&lt;/h2&gt;
&lt;p&gt;Het Nederlandse fiscale stelsel is niet één berekening, maar een web van berekeningen. De inkomstenbelasting kent schijven die verschillen onder en boven de AOW-leeftijd. Daarbovenop liggen de algemene heffingskorting, de arbeidskorting, de ouderenkorting en de inkomensafhankelijke combinatiekorting, die elk bij hun eigen inkomensgrenzen op- of afbouwen. De maximale hypotheek hangt af van de Nibud-financieringslastnormen, de rente, je bestaande schulden, de NHG-grens en zelfs het energielabel van je woning. Toeslagen (zorg, huur, kindgebonden budget, kinderopvang) hebben elk hun eigen afbouwtrajecten en vermogensgrenzen.&lt;/p&gt;
&lt;p&gt;Doordat die regels met elkaar verweven zijn, betekent één vraag goed beantwoorden dat je er meerdere tegelijk moet begrijpen. Maar de tools online behandelen ze als losse eilanden. Je berekent je netto salaris op de ene site, je hypotheek op de andere, je toeslagen op een derde, en geen enkele laat zien hoe een verandering in de één doorwerkt in de rest. Bovenop die versnippering betekende het gebruik ervan vaak dat je persoonlijke financiële gegevens moest afstaan aan sites die ik niet vertrouwde, of dat je je door advertenties heen moest werken naar een getal dat misschien al achterhaald was.&lt;/p&gt;
&lt;h2&gt;De oplossing: Geldhoek&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://geldhoek.nl&quot;&gt;Geldhoek&lt;/a&gt; bundelt deze rekentools op één plek, gebouwd rond drie uitgangspunten: privacy als ontwerpkeuze, één gecontroleerde bron van waarheid voor elk fiscaal cijfer, en volledige transparantie over waar de getallen vandaan komen. De slogan zegt het rechttoe rechtaan: &lt;em&gt;reken je geldvragen zelf uit&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Wat ik heb gebouwd&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;De calculators.&lt;/strong&gt; Geldhoek draait op dit moment zeven tools die dezelfde onderliggende fiscale logica delen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Netto salaris&lt;/strong&gt; rekent je brutoloon om naar netto, met de belastingschijven, de algemene heffingskorting en arbeidskorting, de inkomensafhankelijke combinatiekorting, pensioenpremie, de 30%-regeling voor expats en de bijtelling voor een auto of fiets van de zaak (met de juiste tarieven voor nulemissie- en youngtimerauto&apos;s en een eventuele eigen bijdrage). Een grafiek toont de opbouw van je lastendruk, en een uitklapbare uitleg loopt stap voor stap door elke schijf en korting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bruto vanuit netto&lt;/strong&gt; draait die berekening om, van een gewenst nettoloon naar het benodigde bruto, inclusief de keuze hoe je jaarinkomen verdeeld is over maandsalaris, vakantiegeld en extra&apos;s, en een eventuele bijtelling voor een auto of fiets van de zaak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invloed van ander loon&lt;/strong&gt; is de meest complete tool. Hij rekent twee salarissituaties volledig door en zet ze naast elkaar, inclusief het effect op je netto inkomen, op alle vier de toeslagen (zorg, huur, kindgebonden budget, kinderopvang) en op je maximale hypotheek. Precies het samenspel dat losse tools verbergen, waarbij een loonsverhoging netto iets oplevert maar toeslag kost en je leenruimte verschuift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Toeslagen&lt;/strong&gt; schat zorgtoeslag, huurtoeslag, kindgebonden budget en kinderopvangtoeslag in één formulier, met vermogensgrenzen en een gestapelde afbouwgrafiek die laat zien hoe elke toeslag afneemt naarmate je inkomen stijgt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maximale hypotheek&lt;/strong&gt; past de Nibud-financieringslastnormen toe op één of twee inkomens, met de actuele rente of de toetsrente, bestaande schulden die je per soort aanvinkt, de NHG-grens, de WOZ-waarde en je eigen geld, en energielabel- en verduurzamingsbonussen voor extra leenruimte. Het toont de netto maandlasten na hypotheekrenteaftrek en een gevoeligheidsanalyse die laat zien hoe je leenruimte meebeweegt met rente en inkomen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sparen &amp;#x26; rente&lt;/strong&gt; projecteert samengestelde groei, maar dan flexibel. Je bouwt je inleg op in meerdere fases (per maand, kwartaal, jaar of eenmalig, met negatieve fases voor opnames), kiest hoe vaak rendement wordt bijgeschreven, en vergelijkt meerdere rendementsscenario&apos;s naast elkaar, elk met een optionele bandbreedte (laag, verwacht, hoog). Box 3 kun je meenemen, zowel het huidige forfaitaire stelsel als het voorstel 2028, of allebei om te vergelijken. De uitkomst komt in een interactieve grafiek met onzekerheidsbanden, een uitsplitsing van de bijdrage per inlegfase en een jaar-voor-jaar tabel. Het is de calculator die ik zelf het meest gebruik, om mijn eigen toekomst te plannen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Box 3 2028: koersschommelingen&lt;/strong&gt; simuleert via een Monte Carlo-model (honderden rendementspaden) wat de voorgestelde belasting op werkelijk rendement doet bij grillige beurzen. Het zet het voorstel 2028 naast het huidige forfait en naast afrekenen-bij-verkoop als ijkpunt, toont de belasting per jaar (het forfait heft altijd, het voorstel piekt in goede jaren en zakt naar nul of een teruggaaf in verliesjaren), een waaiergrafiek van je netto vermogen met percentielbanden, en de kans dat je onder het voorstel goedkoper uit bent. Je stelt zelf de verliesverrekening (carry-back) in, en kunt het venijn nalezen: een piek in december gevolgd door een crash in januari, en het overgangsgat rond 2027.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Eén bron van waarheid.&lt;/strong&gt; Elke schijf, korting, norm en grens staat in één getypeerde datalaag, geordend per belastingjaar, zodat elke calculator met identieke cijfers rekent. De data van het huidige jaar is gecontroleerd tegen de primaire bronnen (Belastingdienst, Nibud, Dienst Toeslagen, NHG, Rijksoverheid, CBS) met een vastgelegde controledatum, en het jaarlijkse bijwerkproces is stap voor stap gedocumenteerd zodat de cijfers van volgend jaar er netjes in passen. De toeslagberekeningen zijn getest tegen de officiële rekenvoorbeelden, en de hypotheeklogica tegen het Nibud-referentievoorbeeld, zodat de uitkomst niet alleen aannemelijk is, maar overeenkomt met de bron.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actuele hypotheekrente.&lt;/strong&gt; De standaardrente in de hypotheekcalculator komt van De Nederlandsche Bank. Een klein script haalt vóór elke build het actuele DNB-gemiddelde voor nieuwe woningaankoopleningen op, en valt netjes terug op de laatst bekende waarde als de API niet bereikbaar is, zodat de build nooit struikelt over een netwerkfoutje.&lt;/p&gt;
&lt;h2&gt;Privacy als uitgangspunt&lt;/h2&gt;
&lt;p&gt;Waar het me het meest om ging: Geldhoek krijgt je cijfers nooit te zien. Elke berekening draait volledig in je browser. Voor de bezoekersstatistieken gebruik ik &lt;a href=&quot;https://rybbit.com&quot;&gt;Rybbit&lt;/a&gt;, een privacyvriendelijke, GDPR-conforme analytics-tool zonder cookies. Daarmee zie ik alleen geaggregeerde cijfers, zoals welke pagina&apos;s bezocht worden, en nooit wat jij intikt: het tracken van queryparameters staat uit, dus de staat van de calculator in de URL blijft buiten beeld. Zelfs het lettertype is self-hosted. Het enige dat de site bewaart is wat je in de URL zet: de staat van de calculator wordt in queryparameters gezet, waardoor je een scenario kunt bookmarken of met iemand kunt delen door een link te kopiëren, zonder account of server-side sessie. Je salaris, je hypotheek, jouw situatie: niets daarvan verlaat de pagina.&lt;/p&gt;
&lt;h2&gt;Het technische fundament&lt;/h2&gt;
&lt;p&gt;Geldhoek is gebouwd op Astro en genereert statische HTML tijdens de build. De landingspagina verstuurt nul JavaScript, wat hem snel en goed vindbaar maakt, en de interactieve calculators zijn React-islands die alleen worden gehydrateerd op de pagina&apos;s die ze nodig hebben. De styling is Tailwind v4, de fiscale berekeningen zijn pure TypeScript-functies die losstaan van de UI en gedekt zijn door unit tests, en gestructureerde data op elke calculatorpagina helpt bij de vindbaarheid. Het is een bewust saaie, duurzame stack: statisch waar het kan, interactief alleen waar het moet.&lt;/p&gt;
&lt;h2&gt;Wat ik heb geleerd&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ik begrijp het Nederlandse belastingstelsel nu echt.&lt;/strong&gt; Om elke calculator kloppend te krijgen moest ik de regels tot op de bodem uitpluizen, en pas dan zie je hoeveel er onder de motorkap zit. Dat een maximale hypotheek niet rechtstreeks uit je inkomen volgt maar uit de Nibud-financieringslastnormen, met een toetsrente die los staat van de werkelijke rente, en dat een studieschuld via een wegingsfactor je leenruimte drukt. Dat netto salaris een stapeling is van schijven en vier heffingskortingen die elk bij hun eigen inkomensgrens op- of afbouwen. Dat de toeslagen eigen afbouwtrajecten en vermogensgrenzen hebben die in elkaar grijpen. Die kennis begon als middel om de tool te bouwen, maar is inmiddels een van de waardevolste dingen die het project me heeft opgeleverd.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De Monte Carlo-simulatie was nieuw voor me.&lt;/strong&gt; Voor de box 3-calculator wilde ik laten zien wat koersschommelingen met de aanslag doen, en daarvoor moest ik me verdiepen in Monte Carlo-simulatie: honderden mogelijke rendementspaden trekken uit een verdeling en kijken naar de spreiding van de uitkomsten in plaats van naar één gemiddelde. Een gladde prognose verbergt juist het venijn dat ik wilde tonen. Pas door de paden te simuleren werd zichtbaar hoe een piek in december gevolgd door een crash in januari uitpakt. Een techniek die ik nog niet kende, en die nu in mijn gereedschapskist zit.&lt;/p&gt;
&lt;h2&gt;Huidige status&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://geldhoek.nl&quot;&gt;Geldhoek&lt;/a&gt; is live met zeven calculators en een kleine reeks uitlegartikelen over onderwerpen als waarom een loonsverhoging tegenvalt en wat een huis kopen kost naast de hypotheek. Het is gratis, heeft geen advertenties en vraagt om niets. Het doel blijft hetzelfde: heldere antwoorden op echte Nederlandse geldvragen, gebouwd op cijfers die je tot hun bron kunt herleiden, met data die je eigen browser nooit verlaat.&lt;/p&gt;</content:encoded><category>Astro</category><category>React</category><category>TypeScript</category><category>Tailwind</category></item><item><title>Garen Collectie: een garen-inventaris-app voor mijn moeder</title><link>https://gerwinkuijntjes.nl/nl/projecten/garen-collectie-app-voor-mijn-moeder/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/projecten/garen-collectie-app-voor-mijn-moeder/</guid><description>Een Android-app waarmee mijn moeder haar Gütermann naaigaren-collectie bijhoudt. De app combineert een volledige catalogus van alle 400 garenkleuren, gescraped van een Nederlandse fournituren-webshop, met een persoonlijke inventaris, zodat ze altijd weet welke kleuren ze al heeft voordat ze nieuwe koopt.</description><pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mijn moeder naait veel, en in de loop der jaren heeft ze een flinke collectie Gütermann naaigaren opgebouwd. Het probleem is herkenbaar voor iedereen met een hobby vol materialen: in de winkel wist ze nooit meer welke kleuren ze al thuis had liggen. Zo kocht ze telkens dubbele kleuren die ze al bezat en miste ze juist de kleuren die ze echt nodig had. Een handgeschreven lijstje hield het nooit lang vol, want Gütermann verkoopt zo&apos;n 400 kleuren en veel daarvan lijken sprekend op elkaar.&lt;/p&gt;
&lt;p&gt;Ik wilde haar iets beters geven dan een notitieblok, dus bouwde ik Garen Collectie, een kleine Android-app die één ding goed doet: het complete Gütermann-kleurenpalet tonen en precies bijhouden welke klosjes ze in huis heeft.&lt;/p&gt;
&lt;h2&gt;Het probleem: 400 kleuren en geen overzicht&lt;/h2&gt;
&lt;p&gt;Het naaigaren van Gütermann is er in ongeveer 400 tinten, verkocht in twee klosmaten: kleine klosjes van 200m en grote klosjes van 1000m. De kleuren worden aangeduid met een nummer, niet alleen een naam, en veel ervan zijn subtiele variaties op dezelfde tint. Zonder het klosje in je hand is het echt lastig om &quot;muisgrijs&quot; van &quot;donkergrijs&quot; te onderscheiden, of om te onthouden of je nu 615 of 635 hebt.&lt;/p&gt;
&lt;p&gt;Voor wie garen gaat kopen, levert dat twee terugkerende frustraties op: een kleur kopen die je al hebt, of voor het rek staan zonder te weten of een kleur die je nodig hebt nog in je collectie ontbreekt. Wat ze nodig had, was één overzicht van het hele assortiment, met een duidelijke markering bij de kleuren die ze al bezit.&lt;/p&gt;
&lt;h2&gt;De oplossing: catalogus plus inventaris&lt;/h2&gt;
&lt;p&gt;De app heeft twee schermen. Het eerste is de volledige &lt;strong&gt;catalogus&lt;/strong&gt; van alle 400 Gütermann-kleuren, elk met het officiële nummer, de Nederlandse kleurnaam en een foto van het echte klosje. Het tweede is haar persoonlijke &lt;strong&gt;inventaris&lt;/strong&gt;, de selectie die ze bezit, met de aantallen die ze van elke kleur heeft.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/screens.VF7lm_WV.png&quot; alt=&quot;De catalogus, het kleurdetailscherm en de persoonlijke inventaris&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;De catalogus, het kleurdetailscherm en de persoonlijke inventaris.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Door op een kleur te tikken opent een detailscherm waarin ze kan vastleggen hoeveel kleine (200m) en grote (1000m) klosjes ze heeft, en een kleur kan markeren als bijna op, zodat die op de volgende boodschappenlijst belandt. Het inventarisscherm vat de hele collectie vervolgens in één oogopslag samen: hoeveel kleuren, hoeveel klosjes, en welke bijna leeg zijn.&lt;/p&gt;
&lt;p&gt;Het resultaat is dat de vraag &quot;heb ik deze kleur al?&quot; nu een direct antwoord heeft in haar broekzak, en de vervolgvraag &quot;welke raken bijna op?&quot; net zo goed.&lt;/p&gt;
&lt;h3&gt;Een groeiende collectie beheren&lt;/h3&gt;
&lt;p&gt;Een grote collectie kleur voor kleur bewerken wordt al snel omslachtig, dus het inventarisscherm ondersteunt multiselectie. Ze kan meerdere kleuren tegelijk selecteren en ze allemaal als bijna op markeren zodat ze op de boodschappenlijst belanden, ze weer als aangevuld markeren, of ze in één keer verwijderen.&lt;/p&gt;
&lt;p&gt;Omdat alles op het toestel staat, heb ik ook een eenvoudige back-up en herstel toegevoegd via het menu. De hele inventaris kan naar een JSON-bestand geëxporteerd worden en later teruggezet, of door de huidige collectie te vervangen of door samen te voegen, waarbij de aantallen bij elkaar worden opgeteld. Een nieuwe telefoon of een herinstallatie betekent zo nooit dat de lijst met de hand opnieuw opgebouwd moet worden.&lt;/p&gt;
&lt;h2&gt;De data ophalen: 400 kleuren scrapen&lt;/h2&gt;
&lt;p&gt;Een app als deze is alleen zo goed als zijn catalogus, en ik had geen zin om 400 kleuren en hun foto&apos;s met de hand in te typen. Gütermann publiceert geen nette, machine-leesbare kleurenlijst, dus ik ging op zoek naar een bron die alle kleuren al overzichtelijk had staan, met een consistente foto per tint.&lt;/p&gt;
&lt;p&gt;Die vond ik bij &lt;a href=&quot;https://fourniturenkraam.nl&quot;&gt;fourniturenkraam.nl&lt;/a&gt;, een Nederlandse fournituren-webshop die elke Gütermann-kleur als apart product verkoopt, compleet met een nette studiofoto van het klosje. De webshop draait op Shopify, en dat bleek de sleutel die het scrapen eenvoudig en netjes maakte: elke Shopify-collectie heeft een gepagineerde JSON-feed op &lt;code&gt;/collections/&amp;#x3C;handle&gt;/products.json&lt;/code&gt;. Daardoor hoefde ik nooit broze HTML te parsen, maar kon ik de gestructureerde productdata rechtstreeks uitlezen.&lt;/p&gt;
&lt;p&gt;De losse klosjes van 200m staan allemaal in één collectie, en elke producttitel bevat precies de twee gegevens die ik nodig had:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Gutermann naaigaren | 200m | 000 zwart&quot;   -&gt;  code &quot;000&quot;, naam &quot;zwart&quot;
&quot;Gütermann garen | 200m | 100 donker mint&quot; -&gt;  code &quot;100&quot;, naam &quot;donker mint&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;De scraper blijft daardoor piepklein. Hij pagineert door de JSON-feed, haalt de kleurcode en Nederlandse naam uit het deel van de titel na de laatste &lt;code&gt;|&lt;/code&gt;, en downloadt de eerste productfoto. Dat is in essentie het hele ding:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;while products := json.loads(get(FEED.format(page)))[&quot;products&quot;]:
    for product in products:
        code, _, name = product[&quot;title&quot;].split(&quot;|&quot;)[-1].strip().partition(&quot; &quot;)
        if not code.isdigit() or not product[&quot;images&quot;]:
            continue  # sla de kleurenkaart en alles zonder foto over
        (ASSETS / f&quot;images/{code}.jpg&quot;).write_bytes(get(product[&quot;images&quot;][0][&quot;src&quot;].split(&quot;?&quot;)[0]))
        catalog[code] = {&quot;code&quot;: code, &quot;name&quot;: name, &quot;image&quot;: f&quot;images/{code}.jpg&quot;}
    page += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;De uitvoer is één &lt;code&gt;gutermann-colors.json&lt;/code&gt;-bestand plus een map met afbeeldingen, beide rechtstreeks weggeschreven in de &lt;code&gt;assets/&lt;/code&gt;-map van de app, zodat de app ze offline inleest zonder enige backend. Twee kleine details houden het robuust: de parser slaat elke titel over waarvan het laatste deel niet met een cijfer begint (zo valt het product van de kleurenkaart af), en hij stuurt een eerlijke &lt;code&gt;gutermann-color-scraper/1.0&lt;/code&gt; User-Agent mee, omdat de botbeveiliging van de webshop een &lt;code&gt;403&lt;/code&gt; geeft bij een vervalste browser-variant.&lt;/p&gt;
&lt;p&gt;Het script staat in de repo, zodat de catalogus opnieuw gegenereerd kan worden zodra de webshop een kleur toevoegt of hernoemt. Het gebruikt alleen de Python-standaardbibliotheek, dus er valt niets te installeren:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 scripts/scrape_gutermann_colors.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Het technische fundament&lt;/h2&gt;
&lt;p&gt;Garen Collectie is een native Android-app, geschreven in Kotlin met Jetpack Compose en Material 3 voor de interface. De architectuur is een rechttoe rechtaan MVVM-opzet met een repository-laag. De persoonlijke inventaris staat lokaal in een Room-database (SQLite), terwijl de kleurencatalogus uit de meegeleverde JSON wordt geladen en met Gson wordt geparset. De klosfoto&apos;s komen uit de assets van de app via Coil. Alles draait op het toestel, zonder server en zonder account, wat past bij een app voor één gebruiker die gewoon betrouwbaar moet werken in een stoffenwinkel met slecht bereik.&lt;/p&gt;
&lt;h2&gt;Waarvoor ik het heb gebouwd&lt;/h2&gt;
&lt;p&gt;Dit project ging niet om de techniek. Mijn moeder is de enige gebruiker, en het succes laat zich simpel meten: ze koopt geen garen meer dat ze al heeft en raakt niet meer door de kleuren heen die ze het meest gebruikt. Het bouwen was een mooie aanleiding om een klein, compleet Android-project van begin tot eind te doen, van het scrapen van de brondata tot een verzorgde Compose-interface, maar de echte beloning is dat het echt nuttig is voor iemand die me dierbaar is.&lt;/p&gt;
&lt;h2&gt;Huidige status&lt;/h2&gt;
&lt;p&gt;De app is af en wordt regelmatig gebruikt. De catalogus dekt het volledige Gütermann-assortiment, de inventaris doet precies wat nodig is, en de scraper staat in de repo zodat de kleurenlijst ververst kan worden zodra de bronwinkel verandert. Wil je hem zelf gebruiken, dan kun je de app downloaden via de &lt;a href=&quot;https://github.com/gwku/garen-collectie/releases&quot;&gt;releases-pagina&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Kotlin</category><category>Android</category><category>Jetpack Compose</category><category>Python</category></item><item><title>Zo los je uitstelgedrag voorgoed op: een wetenschappelijke aanpak</title><link>https://gerwinkuijntjes.nl/nl/blog/uitstelgedrag-voorgoed-oplossen/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/blog/uitstelgedrag-voorgoed-oplossen/</guid><description>Ontdek hoe je uitstelgedrag overwint door activeringsenergie te managen en in flow te komen, gebaseerd op principes uit natuurkunde en scheikunde</description><pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We kennen het allemaal. Je hebt een belangrijke taak die je uit moet voeren, maar in plaats van eraan te beginnen, zit je op social media, werk je aan een kleine onbelangrijke taak of heb je ineens besloten dat nu het perfecte moment is om je hele huis te poetsen. Dat is uitstelgedrag en dat heeft niets met lui zijn te maken.&lt;/p&gt;
&lt;h2&gt;Wat is uitstelgedrag eigenlijk?&lt;/h2&gt;
&lt;p&gt;Uitstelgedrag is het vermijden van een belangrijke taak. Zo simpel is het. Maar waarom we deze taken vermijden, dat is pas interessant. Het is namelijk niet zo dat we nergens zin in hebben. Het komt vaak genoeg voor dat we wel ineens aan een andere taak gaan werken en dat kan nog productief zijn ook. Maar toch maken we dan geen voortgang met die ene belangrijke taak.&lt;/p&gt;
&lt;h2&gt;Waarom vermijden we taken?&lt;/h2&gt;
&lt;p&gt;Als je aan het uitstellen bent, komt de taak waar je voor staat meestal over als:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Moeilijk&lt;/strong&gt;: Het vraagt om vaardigheden of kennis waar je niet zeker van bent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Groot&lt;/strong&gt;: Het voelt overweldigend en je weet niet waar je moet beginnen&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Onaangenaam&lt;/strong&gt;: Het is saai, vervelend of iets wat je gewoon niet leuk vindt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je brein, als de efficiëntiezoeker die het is, kiest vanzelf voor makkelijkere, leukere bezigheden. Maar hier komt natuurkunde om de hoek kijken.&lt;/p&gt;
&lt;h2&gt;Newtons eerste wet voor productiviteit&lt;/h2&gt;
&lt;p&gt;Weet je nog die eerste bewegingswet van Newton van de middelbare school?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Een object blijft in rust of beweegt met constante snelheid, tenzij er een externe kracht op inwerkt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Dit geldt perfect voor uitstelgedrag. Als je een taak vermijdt, ben je in rust. Als je ermee bezig bent, ben je in beweging. En hier komt het cruciale inzicht: &lt;strong&gt;in beweging blijven is veel makkelijker dan in beweging komen&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Denk aan het schrijven van een werkstuk. Het moeilijkste deel is niet het schrijven van pagina vijf. Het is het schrijven van de eerste zin. Eenmaal begonnen, bouwt het momentum zich op en wordt doorgaan vanzelfsprekend.&lt;/p&gt;
&lt;h2&gt;Het geheim: gewoon beginnen&lt;/h2&gt;
&lt;p&gt;Dit is de fundamentele waarheid over uitstelgedrag verslaan: &lt;strong&gt;je moet beginnen, en dan doorgaan&lt;/strong&gt;. De eerste wet van Newton zegt immers dat je vanzelf door zal gaan als je eenmaal bent begonnen.&lt;/p&gt;
&lt;p&gt;Maar dat roept natuurlijk de vraag op: als beginnen zo belangrijk is, waarom is het dan zo moeilijk?&lt;/p&gt;
&lt;h2&gt;De scheikunde van uitstelgedrag&lt;/h2&gt;
&lt;p&gt;Hier komt de Arrhenius-vergelijking in beeld, een principe uit de scheikunde dat reactiesnelheden verklaart:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Om iets te laten gebeuren, moet het geactiveerd worden met genoeg energie.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Die hoeveelheid energie wordt &lt;strong&gt;activeringsenergie&lt;/strong&gt; genoemd: de minimale hoeveelheid energie die nodig is om een reactie te laten plaatsvinden.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/activation-energy-graph.DNL_mjfi.jpg&quot; alt=&quot;Activeringsenergie Grafiek&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;De energiebarrière tussen uitstellen en werken — activeringsenergie.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Kijk goed naar deze grafiek. Het laat iets zien over waarom beginnen zo lastig is.&lt;/p&gt;
&lt;p&gt;Links zie je de reactanten, dat ben jij in je huidige staat, lekker onderuitgezakt, door je telefoon aan het scrollen, aan het uitstellen. Dit is een relatief lage energietoestand. Je bent in rust.&lt;/p&gt;
&lt;p&gt;Rechts zie je de producten, dat ben jij actief aan het werk. Dit is een hogere energietoestand omdat je nu mentale en fysieke energie verbruikt. Je bent gefocust, aan het denken, aan het creëren, problemen aan het oplossen. Dit alles kost continue energie.&lt;/p&gt;
&lt;p&gt;Maar hier komt het cruciale punt: om van die lage energietoestand (uitstellen) naar die hoge energietoestand (werken) te komen, moet je eerst over die piek in het midden heen. Die piek is de &lt;strong&gt;activeringsenergie&lt;/strong&gt;, de initiële energieuitbarsting die nodig is om te beginnen.&lt;/p&gt;
&lt;p&gt;Daarom voelt beginnen zo zwaar. Je gaat niet gewoon van rust naar beweging. Je klimt een energieberg op. Je moet door die initiële weerstand heen, de inertie overwinnen, en jezelf activeren tot die werkende staat.&lt;/p&gt;
&lt;p&gt;Denk er eens praktisch over na:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Je laptop openen en naar een leeg document staren - kost activeringsenergie&lt;/li&gt;
&lt;li&gt;Van de bank komen om naar de sportschool te gaan - kost activeringsenergie&lt;/li&gt;
&lt;li&gt;Dat eerste lastige telefoontje plegen naar een moeilijke klant - kost activeringsenergie&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hoe groter de taak aanvoelt, hoe hoger die activeringsenergiepiek wordt. Hoe vermoeider je bent, hoe minder energie je hebt om erover heen te komen.&lt;/p&gt;
&lt;p&gt;Dit verklaart alles. Je bent niet lui. Er is niks mis met je. Je ervaart gewoon een fundamenteel principe uit de scheikunde toegepast op menselijk gedrag. Elke taak heeft een activeringsenergie-barrière, en &lt;strong&gt;uitstelgedrag is simpelweg wat er gebeurt wanneer je beschikbare energie lager is dan de benodigde activeringsenergie&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;De oplossing: energiemanagement&lt;/h2&gt;
&lt;p&gt;Als uitstelgedrag om activeringsenergie draait, zijn er twee mogelijkheden om dit op te lossen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Verhoog je beschikbare energie&lt;/strong&gt; - zodat je meer brandstof hebt om de barrière te overwinnen&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verlaag de benodigde activeringsenergie&lt;/strong&gt; - zodat de barrière kleiner wordt&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Laten we beide mogelijkheden uitwerken.&lt;/p&gt;
&lt;h2&gt;Verhoog je beschikbare energie&lt;/h2&gt;
&lt;p&gt;Zie je beschikbare energie als je vermogen om actie te ondernemen. Dit gaat niet alleen over fysieke, maar ook over mentale energie. Je kunt dit op verschillende manieren verhogen.&lt;/p&gt;
&lt;h3&gt;Verhoog je discipline&lt;/h3&gt;
&lt;p&gt;Je discipline is als een spier: hoe vaker je het bewust gebruikt, hoe sterker het wordt. Maar hier zit de truc: je moet het regelmatig uitdagen, niet alleen als je voor een taak staat die je hebt uitgesteld.&lt;/p&gt;
&lt;p&gt;Daag jezelf regelmatig uit om mentale kracht op te bouwen:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sporten&lt;/strong&gt;&lt;br&gt;
Dit is misschien wel de krachtigste disciplinetrainer die er is. Als je bij die laatste herhaling van je workout bent en je lichaam schreeuwt om te stoppen, maar je toch doorduwt, train je je disciplinespier. Je overschrijft dan je gedachte &quot;ik wil niet&quot; met &quot;ik doe het toch&quot;. Dat is hetzelfde mechanism die je helpt moeilijke taken te starten.&lt;/p&gt;
&lt;p&gt;Denk erover na: elke keer dat je naar de sportschool gaat terwijl je liever thuis zou blijven, oefen je de vaardigheid van beginnen. Elke keer dat je een workout afmaakt terwijl je eerder had kunnen stoppen, bewijs je aan jezelf dat je door weerstand heen kunt duwen. Dit kun je direct toepassen op werktaken.&lt;/p&gt;
&lt;p&gt;Je hoeft geen fitnessfanaat te worden. Zelfs 20 minuten beweging die je uitdaagt bouwt dit vermogen op. De sleutel is consistentie en net iets verder te gaan dan je comfortzone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Koude douches&lt;/strong&gt;&lt;br&gt;
Neem een koude douche. Dit klinkt misschien extreem, maar het is ongelooflijk effectief. Waarom? Omdat een koude douche nemen puur een mentaal spelletje is. Er is geen fysiek gevaar, geen echte schade, alleen ongemak. Elke keer dat je dat koude water in stapt terwijl je brein &quot;NEE&quot; schreeuwt, train je precies het neurale pad dat je nodig hebt voor het starten van moeilijke taken.&lt;/p&gt;
&lt;p&gt;Het stemmetje in je hoofd dat zegt &quot;stap niet dat koude water in&quot; is hetzelfde stemmetje dat zegt &quot;begin niet aan die lastige email&quot; of &quot;start dat project niet.&quot; Door het regelmatig te overschrijven in een situatie zonder risico, bouw je het vermogen op om het te overschrijven wanneer het er écht toe doet.&lt;/p&gt;
&lt;p&gt;Begin met 30 seconden koud water aan het einde van je normale douche. Dat is alles. Gewoon 30 seconden oefenen in &quot;doe het oncomfortabele ding toch maar.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Neem verantwoordelijkheid&lt;/strong&gt;&lt;br&gt;
Als je je committeert aan iemand anders, creëer je externe druk die helpt om activeringsenergie te overwinnen. Dit is waarom mensen vaak beter zijn in het doen van groepswerk voor anderen dan voor zichzelf.&lt;/p&gt;
&lt;p&gt;Sluit je aan bij een studiegroep waar anderen verwachten dat je de stof hebt doorgenomen. Vertel een vriend dat je hem je concept vrijdag stuurt. Plan een vergadering die voorbereiding vereist. Je wilt dan immers de ander niet teleurstellen en je voelt dan meer motivatie om je taak te doen.&lt;/p&gt;
&lt;h3&gt;Verspil minder discipline&lt;/h3&gt;
&lt;p&gt;Discipline is een uitputbare bron. Onderzoek toont aan dat je wilskracht afneemt gedurende de dag naarmate je beslissingen neemt en verleidingen weerstaat. Dit heet keuzestress of beslismoeheid.&lt;/p&gt;
&lt;p&gt;Elke keer dat je besluit wat je aantrekt, wat je eet, of je je telefoon checkt, of welke taak je nu gaat doen, haal je een beetje discipline uit je disciplinereservoir aan. Tegen de tijd dat je voor die belangrijke moeilijke taak staat, heb je misschien het grootste deel van je discipline al gebruikt voor zinloze beslissingen.&lt;/p&gt;
&lt;p&gt;De meeste mensen beginnen de dag door hun smartphone te ontgrendelen en te kiezen welke app ze eerst gaan openen. Dan nemen ze al het nieuws van de nacht door en kiezen ze steeds of het artikel interessant genoeg is om te openen. Vervolgens scrollen ze eindeloos op sociale media om te zien of ze nog iets gemist hebben, waarbij ook weer geldt dat je bij elke post onbewust beslist of deze het waard is om tijd aan te besteden. Je begrijpt wel dat je disciplinereservoir dan al een eind leeg is voordat je ook maar hebt nagedacht over de taken die je vandaag hebt te doen. Stop met discipline verspillen aan dingen die er niet toe doen. Doe eerst je belangrijke taken, en als je het echt nodig vindt, kun je daarna alsnog al die andere dingen doen. Waarschijnlijk heb je er dan de energie niet eens meer voor. Win-winsituatie 🙂&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zet notificaties uit&lt;/strong&gt;&lt;br&gt;
Elke notificatie is een microbeslissing: &quot;Moet ik dit nu checken of negeren?&quot; Zelfs als je het succesvol negeert, heb je discipline gebruikt. Als je het wel checkt, heb je je focus onderbroken en heb je meer discipline nodig om terug te keren naar je taak. En dan duurt het maar liefst minstens 20 minuten om weer te kunnen concentreren op je taak.&lt;/p&gt;
&lt;p&gt;Zet alle niet-essentiële notificaties uit. Niet op stil. Uit. Check je berichten wanneer jij dat kiest, niet wanneer zij ervoor kiezen je te onderbreken. Deze ene verandering kan enorme hoeveelheden mentale energie door de dag heen bewaren.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verminder keuzestress&lt;/strong&gt;&lt;br&gt;
Steve Jobs droeg elke dag hetzelfde outfit. Obama deed dat ook tijdens zijn presidentschap. Mark Zuckerberg doet het nog steeds. Waarom? Omdat beslissen wat je aan trekt wilskracht verbruikt die ze liever besteden aan belangrijke beslissingen.&lt;/p&gt;
&lt;p&gt;Je hoeft niet zo ver te gaan, maar overweeg:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Draag kleren waarbij je niet hoeft na te denken of de combinatie wel goed staat&lt;/li&gt;
&lt;li&gt;Eet zo simpel maar gezond mogelijk zonder bij elke maaltijd te piekeren over elke keuze&lt;/li&gt;
&lt;li&gt;Zorg voor routine waardoor je op de automatische piloot dingen gaat doen en er niet meer over na hoeft te denken&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Het doel is om meer van je leven automatisch te maken. Als je je tanden poetst, debatteer je niet of je het gaat doen of denk je na over hoe je het doet, je doet het gewoon. Hoe meer van je dag zo werkt, hoe meer discipline je bewaart voor wat ertoe doet.&lt;/p&gt;
&lt;p&gt;Denk aan je ochtendroutine. Als je elke ochtend moet beslissen of je gaat sporten, wat je gaat eten, wat je aan trekt, en met welke taak je begint, heb je tientallen beslissingen genomen voordat je überhaupt aan echt werk begint. Beslis in plaats daarvan één keer: &quot;Ik sport om 7 uur, eet eieren en biefstuk, draag mijn maandagse outfit, en begin met de moeilijkste taak.&quot; Nu heb je één beslissing genomen die honderd toekomstige keuzes dekt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Doe belangrijke taken eerst&lt;/strong&gt;&lt;br&gt;
Dit is cruciaal. Je disciplinereservoir is het volst in de ochtend (voor de meeste mensen). Verspil het niet aan e-mail, social media, of makkelijke taken. Pak de moeilijkste taak eerst aan, wanneer je maximale capaciteit hebt.&lt;/p&gt;
&lt;p&gt;E-mail voelt productief, maar het zijn andermans prioriteiten die je dag binnenvallen. Social media voelt als een snelle pauze, maar het is een aandachtsfuik die je focus uitput. Bewaar deze voor nádat je hebt gedaan wat ertoe doet. Als je discipline al is opgemaakt aan het belangrijke werk, maakt het niet uit als je het restje verspilt aan trivialiteiten.&lt;/p&gt;
&lt;h3&gt;Rust goed uit&lt;/h3&gt;
&lt;p&gt;Hier is een paradox: om meer energie voor werk te hebben, moet je stoppen met werken. Rust is niet het tegenovergestelde van productiviteit, het is een voorwaarde ervoor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Slaap goed&lt;/strong&gt;&lt;br&gt;
Dit is de basis van alles. Slaaptekort maakt je niet alleen moe; het tast specifiek de prefrontale cortex aan: het deel van je brein dat verantwoordelijk is voor het overschrijven van impulsen en het starten van moeilijke taken. Met andere woorden, gebrek aan slaap saboteert direct je vermogen om activeringsenergie te overwinnen.&lt;/p&gt;
&lt;p&gt;Je kunt je niet met discipline uit slaaptekort redden. Als je consistent minder dan 7-8 uur krijgt, is uitstelgedrag oplossen bijna onmogelijk. Je activeringsenergie blijft hoog, je beschikbare energie blijft laag, en je vecht een verloren strijd.&lt;/p&gt;
&lt;p&gt;Prioriteer slaap zoals je brandstof in je auto zou prioriteren. Je zou niet proberen door het hele land te rijden met een lege tank. Probeer geen moeilijk werk te doen met een lege slaaptank, maar vul deze op tijd en genoeg.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Breng tijd door in de natuur&lt;/strong&gt;&lt;br&gt;
Dit is geen hippie-onzin; het wordt ondersteund door degelijk onderzoek. Tijd in natuurlijke omgevingen verlaagt cortisol, vermindert mentale vermoeidheid, en herstelt aandachtscapaciteit. Zelfs 20 minuten in een park maakt een meetbaar verschil in je vermogen om daarna te focussen.&lt;/p&gt;
&lt;p&gt;Je hoeft niet in de wildernis te wandelen. Een park werkt. Een straat met bomen werkt. Zelfs een uitzicht op natuur vanuit een raam heeft meetbare effecten. Maak er tijd voor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mediteer&lt;/strong&gt;&lt;br&gt;
Meditatie gaat niet over je geest leegmaken of verlichting bereiken. Het gaat over het oefenen van de vaardigheid om te merken wanneer je aandacht afdwaalt en het zachtjes terug te brengen. Dit is precies de vaardigheid die je nodig hebt voor gefocust werk.&lt;/p&gt;
&lt;p&gt;Als je 10 minuten gaat zitten mediteren, zal je geest tientallen keren afdwalen. Elke keer dat je het merkt en terugkeert naar je ademhaling, versterk je je aandachtscontrole. Dit vertaalt zich direct naar betere focus tijdens het werken.&lt;/p&gt;
&lt;p&gt;Begin met 5-10 minuten per dag. Gebruik een app als dat helpt. Het doel is niet om &quot;goed&quot; te zijn in meditatie, het is om de mentale vaardigheid van aanhoudende aandacht te oefenen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wees creatief&lt;/strong&gt;&lt;br&gt;
Niet alle activiteit put energie uit. Activiteiten die je in een staat van flow brengen - schilderen, muziek maken, tuinieren, iets bouwen, koken kunnen energie herstellen in plaats van het te verbruiken.&lt;/p&gt;
&lt;p&gt;Dit komt omdat creativiteit andere mentale systemen activeert dan degene die je gebruikt voor moeilijk cognitief werk. Als je uren hebt besteed aan code schrijven of data analyseren, voegt 30 minuten schilderen niet toe aan je mentale vermoeidheid, maar het biedt actief herstel.&lt;/p&gt;
&lt;p&gt;Vind iets creatiefs dat je boeit. Het hoeft niet artistiek te zijn. Houtbewerking is creatief. Koken is creatief. Tuinieren is creatief. De sleutel is dat het je aandacht vasthoudt zonder je discipline uit te putten.&lt;/p&gt;
&lt;h3&gt;Eet voedzaam&lt;/h3&gt;
&lt;p&gt;Je brein is ongeveer 2% van je lichaamsgewicht maar verbruikt ongeveer 20% van je calorieën. Het is een energiehongerig orgaan, en het presteert anders afhankelijk van wat je het voedt.&lt;/p&gt;
&lt;p&gt;Dit gebeurt er met typisch modern eten:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Suikerrijk ontbijt → bloedsuikerpiek → insulinerespons → crash tegen 10 uur&lt;/li&gt;
&lt;li&gt;Lunch overslaan of simpele koolhydraten → nog een piek en crash&lt;/li&gt;
&lt;li&gt;Energiedrankje voor &quot;energie&quot; → cafeïne en suiker maskeren vermoeidheid, maken het dan erger&lt;/li&gt;
&lt;li&gt;Tegen de tijd dat je gefocust werk moet doen, zit je energie op een achtbaan&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Het alternatief:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Eieren en vlees bij ontbijt → stabiele bloedsuiker voor uren&lt;/li&gt;
&lt;li&gt;Dierlijke proteïne en vetten bij elke maaltijd → consistente energie zonder crashes&lt;/li&gt;
&lt;li&gt;Goede hydratatie → betere cognitieve functie&lt;/li&gt;
&lt;li&gt;Onbewerkt voedsel (vlees, eieren, zuivel, fruit) → aanhoudende energie in plaats van pieken en dalen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je hebt geen perfect dieet nodig. Je hebt een stabiel dieet nodig. Dierlijke proteïne, gezonde vetten uit vlees en zuivel, fruit voor snelle energie, genoeg water.&lt;/p&gt;
&lt;h2&gt;Verlaag activeringsenergie: gebruik een katalysator&lt;/h2&gt;
&lt;p&gt;Een andere oplossing om over de energie-barrière heen te komen is om die barrière te verlagen. In de scheikunde verlaagt een katalysator de activeringsenergie die nodig is voor een reactie, zonder zelf verbruikt te worden in het proces.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/activation-energy-catalyst-graph.CbBoLO1z.jpg&quot; alt=&quot;Activeringsenergie met Katalysator&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Een katalysator verlaagt de energiebarrière, waardoor dezelfde reactie makkelijker te starten is.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let op hoe de katalysator een alternatieve route creëert die minder energie nodig heeft om te starten. Het beginpunt (reactanten) en eindpunt (producten) blijven hetzelfde, maar de piek in het midden - de activeringsenergie-barrière - wordt aanzienlijk lager.&lt;/p&gt;
&lt;p&gt;Dit is precies wat je nodig hebt voor je taken. Je hebt katalysatoren nodig die de activeringsenergie verlagen, zodat je minder energie hoeft te gebruiken om productief te zijn. Zo doe je dat:&lt;/p&gt;
&lt;h3&gt;Breek taken op in kleinere taken&lt;/h3&gt;
&lt;p&gt;Dit is de krachtigste katalysator die er is. Zo werkt het:&lt;/p&gt;
&lt;p&gt;Als je kijkt naar &quot;schrijf een rapport,&quot; ziet je brein een enorme, ongedefinieerde taak. Hoe lang duurt het? Wat als je vastloopt? Wat als het niet goed genoeg is? Al deze onzekerheden voegen toe aan de activeringsenergie. De taak voelt als het beklimmen van een berg.&lt;/p&gt;
&lt;p&gt;Maar als je kijkt naar &quot;schrijf één zin,&quot; ziet je brein iets beheerbaar. Eén zin kost 30 seconden. Je weet dat je het kunt. Je ziet het einde vanaf het begin. De activeringsenergie keldert.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In plaats van &lt;code&gt;schrijf een werkstuk&lt;/code&gt; (activeringsenergie: enorm), maak het &lt;code&gt;schrijf één zin&lt;/code&gt; (activeringsenergie: minimaal).&lt;/li&gt;
&lt;li&gt;In plaats van &lt;code&gt;maak het huis schoon&lt;/code&gt; (overweldigend), maak het &lt;code&gt;maak het aanrecht 2 minuten schoon&lt;/code&gt; (haalbaar).&lt;/li&gt;
&lt;li&gt;In plaats van &lt;code&gt;studeer voor het examen&lt;/code&gt; (vaag en enorm), maak het &lt;code&gt;lees en vat één pagina samen&lt;/code&gt; (specifiek en klein).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zodra je één zin schrijft, is de tweede zin makkelijker. Waarom? Omdat je al in beweging bent. Je hebt de activeringsenergie-barrière al overwonnen. Je hebt het document al open, je gedachten georganiseerd, je vingers op het toetsenbord. De tweede zin schrijven kost bijna geen extra energie: je gaat gewoon door.&lt;/p&gt;
&lt;p&gt;Dit is Newtons wet in actie. Vanuit rust starten vereist enorme energie. Beweging voortzetten vereist veel minder. Door de eerste stap minuscuul te maken, maak je beginnen mogelijk. Dan draagt momentum je verder dan je van plan was te gaan.&lt;/p&gt;
&lt;p&gt;Het mooie hiervan? Je schrijft één zin, en plots merk je een typfout en herstel je het. Dan denk je hoe je door kunt gaan en voeg je een alinea toe. Dan realiseer je je dat je een bron moet checken. Dertig minuten later heb je substantieel werk verricht. Niet omdat je enorme discipline had, maar omdat je de barrière laag genoeg maakte om te beginnen.&lt;/p&gt;
&lt;h3&gt;Verlaag je verwachtingen&lt;/h3&gt;
&lt;p&gt;Perfectionisme is een van de meest verraderlijke vormen van uitstelgedrag omdat het zich vermomt als hoge standaarden. Maar hier is het ding: perfectionisme verhoogt activeringsenergie zozeer dat het je vaak helemaal belet te beginnen.&lt;/p&gt;
&lt;p&gt;Als je taak is &quot;schrijf een perfecte inleiding,&quot; is de activeringsenergie massief. Je brein weet dat dit onmogelijk is bij de eerste poging, dus het verzet zich tegen beginnen. De kloof tussen waar je bent (niets geschreven) en waar je moet zijn (perfectie) voelt onoverbrugbaar.&lt;/p&gt;
&lt;p&gt;Maar als je taak is &quot;schrijf een rommelige eerste versie die niemand ooit zal zien,&quot; daalt de barrière dramatisch. Een rommelige versie is makkelijk. Iedereen kan slecht schrijven. De activeringsenergie is minimaal.&lt;/p&gt;
&lt;p&gt;Je kunt geen blanco pagina bewerken, want er staat niets op. Een verschrikkelijke eerste versie is oneindig beter dan een perfecte ongeschreven versie omdat je dan kunt verbeteren wat bestaat. Je kunt niet verbeteren wat niet bestaat.&lt;/p&gt;
&lt;p&gt;Geef jezelf expliciete toestemming om troep te produceren. Vertel jezelf: &quot;Ik ga de slechtst mogelijke inleiding schrijven, gewoon om woorden op de pagina te krijgen.&quot; Dit haalt het psychologische gewicht weg. Je beklimt geen berg meer, maar je zet gewoon een stap.&lt;/p&gt;
&lt;p&gt;Veel van de grootste schrijvers uit de geschiedenis produceerden verschrikkelijke eerste versies.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;the first draft of anything is shit.&quot; (de eerste versie van wat dan ook is waardeloos) — Ernest Hemingway&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Creatie gebeurt in twee verschillende fases: genereren en verfijnen. Perfectionisme probeert beide tegelijk te doen. Dat is tegelijkertijd proberen te schilderen én je kwasten schoon te maken. Scheid ze. Eerst genereren zonder oordeel. Dan verfijnen.&lt;/p&gt;
&lt;h3&gt;Denk aan je waarom&lt;/h3&gt;
&lt;p&gt;Deze strategie werkt anders dan de anderen. Het verlaagt de activeringsenergie-barrière niet direct, maar het verhoogt je motivatie om ertegen op te klimmen. Als de barrière echt hoog is en je kunt hem niet verder verlagen, helpt denken aan je doel om de energie te vinden om erdoorheen te duwen.&lt;/p&gt;
&lt;p&gt;Waarom schrijf je dit werkstuk? Niet &quot;omdat het moet&quot;, dat is niet motiverend, maar stressvol. Misschien ben je oprecht nieuwsgierig naar het onderwerp. Misschien is dit werkstuk onderdeel van een opleiding die leidt naar een carrière die je wilt. Misschien maakt het beheersen van deze vaardigheid je beter in iets waar je om geeft.&lt;/p&gt;
&lt;p&gt;Waarom sport je? Niet &quot;omdat ik moet&quot;, dat werkt zelden. Maar misschien omdat je energie wilt hebben om met je kinderen te spelen. Misschien omdat je je geweldig voelde de laatste keer dat je in vorm was. Misschien omdat je traint voor iets specifieks.&lt;/p&gt;
&lt;p&gt;Waarom bel je met potentiële klanten? Niet &quot;omdat ik moet.&quot; Maar misschien omdat elk gesprek je dichter bij financiële onafhankelijkheid brengt. Misschien omdat je iets bouwt wat voor jou belangrijk is. Misschien omdat je een vaardigheid ontwikkelt die je decennia kunt gebruiken.&lt;/p&gt;
&lt;p&gt;Verbind de taak met iets wat voor jou echt belangrijk is. Dit maakt beginnen niet makkelijk, maar het maakt je bereid om het moeilijke ding toch te doen. Het is het verschil tussen &quot;ik wil dit niet doen&quot; en &quot;ik wil dit niet doen, maar ik doe het toch omdat het ertoe doet.&quot;&lt;/p&gt;
&lt;h2&gt;Discipline vs. motivatie&lt;/h2&gt;
&lt;p&gt;Een cruciaal onderscheid dat veel mensen missen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Discipline zorgt dat je begint&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motivatie zorgt dat je doorgaat&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mensen wachten vaak tot ze zich &quot;gemotiveerd voelen&quot; voor ze beginnen. Dit is achterstevoren. Motivatie veroorzaakt geen actie - actie veroorzaakt motivatie.&lt;/p&gt;
&lt;p&gt;Je hebt discipline nodig om de activeringsenergie te overwinnen. Dit is een bewuste, vaak oncomfortabele toepassing van wilskracht. Je duwt jezelf over die initiële heuvel, ook al heb je er geen zin in. Dit deel is altijd moeilijk. Daar is geen ontkomen aan.&lt;/p&gt;
&lt;p&gt;Maar eenmaal in beweging verschuift er iets. Je ziet vooruitgang. De taak wordt minder abstract en concreter. Ideeën stromen. Problemen lossen zichzelf op. Momentum bouwt op. En plotseling heb je geen discipline meer nodig: je hebt motivatie. Je bent geïnteresseerd in wat je aan het doen bent. Je wilt doorgaan. Het werk heeft zijn eigen momentum.&lt;/p&gt;
&lt;p&gt;Daarom is beginnen zo belangrijk. Je probeert niet motivatie uit het niets op te roepen. Je gebruikt discipline om te beginnen, wat motivatie genereert. Motivatie is het resultaat van actie, niet de oorzaak.&lt;/p&gt;
&lt;p&gt;Denk maar eens na: heb je ooit weleens opgezien tegen een taak, jezelf gedwongen om te starten, en na 1.5 uur ineens opgemerkt dat je al gigantisch ver was met je taak en heerlijk bezig was? Dat is dit principe in actie. De motivatie kwam na de start, niet ervoor.&lt;/p&gt;
&lt;h2&gt;De flow state&lt;/h2&gt;
&lt;p&gt;Wanneer je succesvol begint en doorgaat, gebeurt er iets moois: je komt in een flow. Dit is volledig gefocust zijn op één taak of activiteit, waar tijd lijkt te verdwijnen, afleiding wegvalt, en het werk moeiteloos aanvoelt.&lt;/p&gt;
&lt;p&gt;Flow is de beloning voor het overwinnen van activeringsenergie. Het is de staat waar je niet langer vecht om gefocust te blijven, je wordt getrokken in het werk. Uren gaan voorbij als minuten. Je kijkt op en realiseert je dat je meer hebt bereikt dan je verwachtte met minder moeite dan je vreesde.&lt;/p&gt;
&lt;p&gt;In onze scheikundige analogie: eenmaal over de activeringsenergiepiek geklommen en de &quot;product&quot;-staat (actief werken) bereikt, kost het onderhouden van die staat veel minder energie dan de initiële klim. Je bent in een hogere energietoestand (werken), maar daar blijven is makkelijk omdat je momentum hebt.&lt;/p&gt;
&lt;p&gt;Maar - en dit is cruciaal - je kunt alleen flow bereiken door eerst te beginnen. Flow gebeurt niet terwijl je aan het uitstellen bent. Het gebeurt niet terwijl je nadenkt over beginnen. Het gebeurt nadat je door de weerstand duwt en begint.&lt;/p&gt;
&lt;p&gt;Dit is waarom alle energiemanagementstrategieën ertoe doen. Ze gaan niet alleen over beginnen mogelijk maken, ze gaan erover om flow bereiken mogelijk te maken, waar echte vooruitgang gebeurt met verrassend gemak.&lt;/p&gt;
&lt;h2&gt;Alles samenbrengen&lt;/h2&gt;
&lt;p&gt;Uitstelgedrag is geen karakterfout of persoonlijkheidstrek. Het is een energiemanagementprobleem. Je vermijdt taken omdat de benodigde activeringsenergie je beschikbare energie overstijgt.&lt;/p&gt;
&lt;p&gt;Zodra je dit begrijpt, wordt de oplossing systematisch:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Zorg goed voor jezelf&lt;/strong&gt;: bouw je beschikbare energie op door disciplinetraining, echte rust, en stabiele voeding. Je hebt brandstof in de tank nodig om barrières te overwinnen. Dit is niet optioneel of secundair, het is de basis.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verlaag de drempel&lt;/strong&gt;: verminder activeringsenergie door taken af te breken tot belachelijk kleine eerste stappen, jezelf toestemming geven om verschrikkelijke eerste pogingen te produceren, en te verbinden met je echte doel. Maak beginnen zo makkelijk dat je geen nee kunt zeggen.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Begin gewoon&lt;/strong&gt;: gebruik je discipline om over die initiële heuvel te komen. Accepteer dat het oncomfortabel zal zijn. Doe het toch. Onthoud: je hebt alleen discipline nodig voor de eerste paar minuten. Daarna nemen momentum en motivatie het over.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ga door&lt;/strong&gt;: laat momentum je meevoeren naar flow. Eenmaal in beweging, wordt in beweging blijven natuurlijk. Het moeilijke deel is voorbij. Nu ga je gewoon door.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Onthoud Newtons eerste wet: een object in beweging blijft in beweging. Jouw taak is niet om uren lang piekmotivatie vast te houden. Jouw taak is om te beginnen. De rest volgt vanzelf.&lt;/p&gt;
&lt;p&gt;Stop met wachten op het perfecte moment waarop je gemotiveerd en energiek en klaar zult zijn. Dat moment bestaat niet. Motivatie wordt gegenereerd door actie, niet andersom.&lt;/p&gt;
&lt;p&gt;Beheer in plaats daarvan je energie, verlaag de drempel, en zet de eerste kleine stap. Dan nog een. Dan nog een. Voor je het weet ben je niet aan het uitstellen, maar heb je grote voortgang gemaakt met je taak.&lt;/p&gt;
&lt;p&gt;Zo los je uitstelgedrag voorgoed op.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Welke taak heb je liggen? Breek het af tot de kleinst mogelijke eerste stap, verlaag de activeringsenergie, en begin vandaag. Je zult versteld staan hoe snel momentum opbouwt zodra je over die initiële heuvel bent.&lt;/p&gt;</content:encoded><category>Productiviteit</category><category>Zelfontwikkeling</category></item><item><title>StaticForm: formulierverwerker met spamfiltering en volledig inzicht</title><link>https://gerwinkuijntjes.nl/nl/projecten/staticform-betrouwbare-formulieren-voor-elke-website/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/projecten/staticform-betrouwbare-formulieren-voor-elke-website/</guid><description>Formulierverwerking voor websites, met meer dan tien spamcontroles, een volledig actielogboek en directe meldingen als er iets misgaat. Ontstaan uit frustratie over het telkens opnieuw bouwen van dezelfde formulierverwerker voor klanten. Inmiddels in gebruik bij developers en agencies die formulieren beheren voor meerdere sites.</description><pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bij het bouwen van websites voor klanten liep ik telkens tegen dezelfde twee problemen aan. Het eerste was spam. Contactformulieren, nieuwsbriefinschrijvingen, sollicitatieformulieren: ze trokken allemaal bots aan zodra ze live gingen. Klanten belden om te zeggen dat ze tientallen inzendingen per dag kregen, waarvan er vrijwel geen enkele echt was. Het tweede probleem was subtieler: formulieren die er prima uitzagen maar het niet waren. E-mails of webhooks werden niet afgeleverd en niemand merkte het, totdat een klant vroeg waarom ze al weken niets van klanten hadden gehoord.&lt;/p&gt;
&lt;p&gt;Veel projecten hadden een formulierverwerker nodig, en elke formulierverwerker vroeg om spambescherming, betrouwbare bezorging en een manier om te weten wanneer er iets misging. Ik bouwde telkens dezelfde oplossing opnieuw. Daarom heb ik &lt;a href=&quot;https://staticform.app&quot;&gt;StaticForm&lt;/a&gt; gebouwd.&lt;/p&gt;
&lt;h2&gt;Twee problemen die eenvoudig lijken tot ze dat niet zijn&lt;/h2&gt;
&lt;p&gt;Vrijwel elke website heeft formulieren nodig. Een HTML-formulier bouwen kost minuten. Het betrouwbaar maken is waar het echte werk begint.&lt;/p&gt;
&lt;p&gt;Spam was het meest zichtbare probleem. Binnen dagen na het live gaan van een formulier begon de inbox vol te lopen met rommel. Bots bestoken publieke endpoints continu, en hun inzendingen zijn niet te onderscheiden van echte berichten. Zo&apos;n 95% van het formulierverkeer op het web is spam, en je weet het pas als je elk bericht apart opent en leest, dus dat was grotendeels wat klanten zaten te doen. Daardoor voelde het formulier nutteloos aan, ook al werkte het technisch gewoon.&lt;/p&gt;
&lt;p&gt;Het tweede probleem was stiller van aard, en daardoor lastiger op te merken. Een formulier kon volledig kapot zijn zonder dat iemand het doorhad. E-mailbezorging mislukte zonder enige foutmelding. Een webhook tikte een timeout aan. Het formulier toonde nog steeds &quot;bedankt&quot; aan wie het invulde, maar de inzending verdween gewoon. Ik had klanten die weken lang niet doorhadden dat er geen aanvragen meer binnenkwamen, niet omdat niemand contact opnam, maar omdat niets meer aankwam.&lt;/p&gt;
&lt;p&gt;Daarboven op had ik bij elk project ook te maken met versnippering over platforms. Static site generators, WordPress, GitHub Pages, Netlify en Vercel vragen allemaal dezelfde backendoplossing, maar via totaal verschillende routes. En elke oplossing stond los van de andere, zonder gedeeld overzicht.&lt;/p&gt;
&lt;h2&gt;De oplossing: StaticForm&lt;/h2&gt;
&lt;p&gt;Ik heb &lt;a href=&quot;https://staticform.app&quot;&gt;StaticForm&lt;/a&gt; gebouwd om dit allemaal op één plek te regelen. Het idee is eenvoudig: stel een formulier in via het dashboard, kopieer de endpoint-URL en zet die in het action-attribuut van je formulier. Je kiest waar inzendingen naartoe moeten (e-mail, Slack, Discord, een webhook) en StaticForm regelt de rest, inclusief het opslaan van de inzending in Europa ongeacht of de notificatie aankomt. De hele setup duurt zo&apos;n vijf minuten.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/sf-inbox-light.CSI13CPC.png&quot; alt=&quot;StaticForm inzendingen inbox&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;De inzendingen inbox — alle formulierinzendingen op één plek.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Wat ik heb gebouwd&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://staticform.app&quot;&gt;StaticForm&lt;/a&gt; bouwen draaide om één ding: alle storingen aanpakken die in de praktijk voor problemen zorgen, terwijl de setup eenvoudig genoeg blijft om er niet over na te hoeven denken.&lt;/p&gt;
&lt;h3&gt;Spamfiltering&lt;/h3&gt;
&lt;p&gt;Spambescherming werd het onderdeel waar ik verreweg de meeste tijd aan besteedde, omdat het het moeilijkst goed te doen is op schaal. Er draaien nu meer dan tien controles op elke inzending: lokvelden die eenvoudige bots vangen, blokkering op basis van IP-reputatie, detectie van datacenter-IP-adressen (de meeste bots draaien via cloudproviders, niet via thuisverbindingen), filtering op wegwerp-e-mailadressen, controles op domeinleeftijd, e-maildomeinvalidatie, inhoudsanalyse op verdachte patronen, en detectie van ongewenste verkoopberichten: het soort &quot;ik zag dat uw website beter gevonden kan worden in Google&quot;-berichten dat grotendeels uit automatisch gegenereerde sjablonen bestaat.&lt;/p&gt;
&lt;p&gt;Wat ik bijzonder interessant vind is de misleidingslaag. StaticForm stuurt spam-inzendingen een 200 OK terug in plaats van een fout. Bots die fouten krijgen passen zich aan; ze proberen andere payloads, wisselen van IP, veranderen tactiek. Bots die een 200 OK krijgen denken dat ze geslaagd zijn en gaan door. Na verloop van tijd versterkt dit effect zich: spambronnen stoppen met het aanvallen van je endpoint zonder te weten dat ze gefilterd worden. Onderschepte inzendingen verdwijnen voordat ze een notificatiekanaal bereiken.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/sf-spam-detail-light.BWw9_QrJ.png&quot; alt=&quot;Spaminzending detail met filteringsreden&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Een spaminzending in detail, met welk filter het heeft onderschept en waarom.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Volledig Inzicht&lt;/h3&gt;
&lt;p&gt;Het probleem dat ik telkens tegenkwam met bestaande formuliertools was niet alleen spam, maar ook het gebrek aan zicht op wat er fout ging. E-mailbezorging mislukt stilletjes. Webhooks lopen vast zonder dat iemand een melding krijgt. Een inzending komt nergens terecht, en niemand weet het totdat een klant meldt dat hij al twee weken niets van klanten heeft gehoord.&lt;/p&gt;
&lt;p&gt;StaticForm legt elke actie per inzending vast, met resultaat en eventuele foutdetails. Als e-mailbezorging mislukt, zie je precies waarom (SMTP-fout, mailbox vol, geweigerd door de ontvangende server) en wanneer het gebeurde. Als een webhook uitvalt, staat dat er ook bij. Bij een storing krijg je meteen een melding, niet pas wanneer de klant erover begint.&lt;/p&gt;
&lt;p&gt;Elke inzending wordt eerst opgeslagen, voordat er een notificatie uitgaat. Daardoor kun je een mislukte notificatie met één klik opnieuw versturen vanuit het dashboard, naar een inzending die op het moment van binnenkomst al veilig was weggeschreven. Niets gaat verloren doordat een afhankelijke dienst een slechte dag had.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/sf-log-detail-light.CPf2qPkv.png&quot; alt=&quot;Actielogboek met bezorgingsfout details&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Een actielogboek met een mislukte bezorgpoging en de exacte foutmelding.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Notificaties en bezorging&lt;/h3&gt;
&lt;p&gt;E-mail valt vaker uit dan mensen denken. SMTP-servers gaan offline, inboxen lopen vol, berichten belanden in de spammap. Daarom ondersteunt &lt;a href=&quot;https://staticform.app&quot;&gt;StaticForm&lt;/a&gt; meerdere notificatiekanalen tegelijk. Je kunt inzendingen sturen naar e-mail én Slack én Discord én een eigen webhook, welke combinatie ook het meest logisch is voor het project.&lt;/p&gt;
&lt;p&gt;Je kunt naast formulierinzendingen ook bestanden ontvangen. Sollicitaties met cv&apos;s, contactformulieren met bijlagen, alles waarbij je documenten wilt ontvangen. Bestanden worden veilig opgeslagen en zijn te downloaden via het dashboard.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/sf-submit-actions-light.B3Lip8Lo.png&quot; alt=&quot;Acties configureren voor meerdere notificatiekanalen&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Acties ingesteld om inzendingen gelijktijdig naar meerdere kanalen te sturen.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Eenvoudige integratie&lt;/h3&gt;
&lt;p&gt;Voor een developer is de integratie simpelweg een HTML-formulier dat naar een endpoint-URL wijst. Je bouwt het formulier zoals je dat altijd zou doen, en StaticForm neemt het daarna over. Het dashboard genereert kant-en-klare code voor plain HTML en een versie met het staticform.js-hulpscript dat validatie, laadindicatie en foutmeldingen meteen afhandelt. Voor framework-specifieke code is er ook een kant-en-klare AI-prompt die je aan Claude, Codex of een andere AI-assistent naar keuze kunt geven. Het werkt op Jekyll, Hugo, Gatsby, Next.js, Astro, GitHub Pages, Netlify, Vercel, Cloudflare Pages, WordPress, of elk platform dat een POST request naar een endpoint kan versturen.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/sf-integration-tab.b0lud4Ly.png&quot; alt=&quot;Integratievenster met gegenereerde code snippets&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Het integratievenster met kant-en-klare code snippets voor verschillende setups.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Het technische fundament&lt;/h3&gt;
&lt;p&gt;Achter die eenvoudige setup zit infrastructuur die is gebouwd op betrouwbaarheid. De API-gateway verwerkt elke inzending met snelheidsbegrenzing, validatie en wachtrijverwerking. Bij verkeerspieken gaat niets verloren. Als een afhankelijke dienst traag is, wachten inzendingen in de wachtrij totdat verwerking mogelijk is.&lt;/p&gt;
&lt;p&gt;De architectuur is gebouwd rond één principe: data wordt altijd als eerste opgeslagen. Pas daarna gaan notificaties uit. Zelfs als het volledige notificatiesysteem uitvalt, gaat er geen inzending verloren.&lt;/p&gt;
&lt;p&gt;Alles draait in containers via CI/CD-pipelines. De infrastructuur is volledig in code vastgelegd, zodat het volledige platform herstelbaar is vanuit configuratiebestanden. Monitoring en alerting draaien dag en nacht. Het platform haalt 99,9% uptime doordat het is gebouwd om storingen op te vangen: services herstarten automatisch, wachtrijen bufferen verkeerspieken, databaseverbindingen zijn gepoold en snelheidsbegrenzing voorkomt misbruik.&lt;/p&gt;
&lt;h2&gt;Gebouwd voor het beheren van meerdere sites&lt;/h2&gt;
&lt;p&gt;Het dashboard is ook ingericht voor het beheren van veel formulieren en klanten vanuit één plek. Elk formulier is te taggen per klant of site, zodat je één overzicht hebt van alle klantformulieren (inzendingen, statussen, recente fouten) zonder van account te wisselen. Je kunt klanten uitnodigen zodat ze zelf toegang hebben, of notificaties rechtstreeks naar hen sturen zodat ze op de hoogte blijven zonder in te hoeven loggen, of beide.&lt;/p&gt;
&lt;p&gt;Hier is volledig inzicht het allerbelangrijkst. Als het formulier van een klant stilletjes stukgaat, laat het actielogboek dat zien. Je kunt het probleem oplossen voordat de klant het merkt. En omdat je een vast maandelijks bedrag betaalt ongeacht hoeveel formulieren of klanten je toevoegt, zijn de kosten per project voor formulierverwerking feitelijk nul.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/sf-dashboard-light.DIuvAjYv.png&quot; alt=&quot;Dashboard overzicht van meerdere formulieren en klanten&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Het dashboardoverzicht van meerdere formulieren en klanten.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Wat ik heb geleerd&lt;/h2&gt;
&lt;p&gt;StaticForm bouwen liet me zien dat productwerk fundamenteel anders is dan klantwerk:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Productbeslissingen zijn moeilijker dan technische keuzes.&lt;/strong&gt; De technologiekeuzes waren eenvoudig. Het moeilijke was de prijs bepalen, uitvinden welke functies er echt toe doen en beslissen wat prioriteit krijgt. Er is geen klant die je dat vertelt, in elk geval niet als je net begint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Marketing is lastiger dan bouwen.&lt;/strong&gt; Een formulierverwerker bouwen kan ik in een paar weken. Mensen genoeg vertrouwen geven om het ook echt te gebruiken? Dat is de echte uitdaging. Ik leer waarde te bieden op platforms zoals Reddit, contact te leggen met agencies, developers en communities, en echte gesprekken het werk te laten doen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Onderhoud stopt nooit.&lt;/strong&gt; Klantprojecten hebben vaak een duidelijk eindpunt. Een SaaS-product draait voor altijd. Elke gebruiker verwacht 99,9% uptime. Die verantwoordelijkheid kleurt alles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Eenvoud wint van features.&lt;/strong&gt; Mijn eerste neiging was zoveel mogelijk functies inbouwen. Maar wat StaticForm nuttig maakt, is dat het een paar dingen extreem goed doet. Minder complexiteit betekent meer betrouwbaarheid.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;De prijs telt.&lt;/strong&gt; Ik begon met een creditmodel waarbij spam niet meetelde voor het saldo van de klant. Een redelijke aanpak, maar het zorgde voor wrijving; potentiële gebruikers moesten van tevoren inschatten hoeveel credits ze nodig hadden. Het huidige model is gebouwd rond het idee van betalen voor functies, niet voor inzendingen. Beide abonnementen bevatten onbeperkte inzendingen en formulieren. Wat wél begrensd is, zijn onderdelen met echte kosten per eenheid: e-mailacties (25.000 per maand op Starter, onbeperkt op Pro) en bestandsopslag. Spam telt nooit mee voor die grenzen. Dat geeft de juiste prikkel: ik heb er alle belang bij om spam goed te filteren, want wat er toch doorheen glipt kost mij geld, niet de klant.&lt;/p&gt;
&lt;h2&gt;Huidige status&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://staticform.app&quot;&gt;StaticForm&lt;/a&gt; is live en verwerkt inzendingen voor betalende klanten in meerdere landen. Het platform haalt 99,9% uptime en verwerkt alles van eenvoudige contactformulieren tot complexe sollicitaties met bestandsuploads.&lt;/p&gt;
&lt;p&gt;De prijsstelling is een vast maandelijks bedrag met onbeperkte inzendingen. Uitproberen kan zonder creditcard. Een WordPress-plugin is in ontwikkeling voor sites die een native CMS-integratie willen.&lt;/p&gt;
&lt;p&gt;Het doel blijft hetzelfde: spam die wordt tegengehouden, volledig inzicht in elke inzending, een eerlijk tarief zonder verrassingen en een integratie die in minuten staat. Bekijken kan op &lt;a href=&quot;https://staticform.app&quot;&gt;staticform.app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Feedback, feature requests en ervaringen zijn altijd welkom. Neem gerust contact op bij ideeën of problemen.&lt;/p&gt;</content:encoded><category>.NET</category><category>C#</category><category>Nuxt</category></item><item><title>P1 Meter Monitor: automatische energiemonitoring en rapportage</title><link>https://gerwinkuijntjes.nl/nl/projecten/p1-meter-monitor-automatische-energiemonitoring/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/projecten/p1-meter-monitor-automatische-energiemonitoring/</guid><description>Een Python-service in Docker die metingen verzamelt van een P1 slimme meter, opslaat in een QuestDB tijdreeksdatabase, en maandelijks een verbruiksrapport verstuurt inclusief teruglevering van zonnepanelen en vergelijking met de vorige maand.</description><pubDate>Sat, 15 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;De meeste huishoudens in Nederland hebben inmiddels een slimme meter met een P1-poort: een gestandaardiseerde data-interface die real-time elektriciteits- en gasmetingen beschikbaar maakt. Met een kleine dongle zoals de HomeWizard P1 wordt die data toegankelijk als JSON API op je lokale netwerk. De metingen zijn er, maar zonder iets dat ze verzamelt en opslaat, gaan ze verloren. Ik wilde een systeem dat stilletjes elk datapunt zou verzamelen, opslaan in een echte tijdreeksdatabase, en me eens per maand zou vertellen wat mijn huishouden daadwerkelijk had verbruikt, hoeveel er via de zonnepanelen was teruggeleverd aan het net, en hoe dat zich verhoudt tot de maand ervoor.&lt;/p&gt;
&lt;h2&gt;Het probleem met bestaande opties&lt;/h2&gt;
&lt;p&gt;Er zijn genoeg apps voor energiemonitoring, maar de meeste komen met compromissen die ik niet wilde. Sommige vereisen cloudaccounts en sturen je data naar externe servers. Andere bieden dashboards maar geen gestructureerde rapportage. De HomeWizard-app zelf toont live metingen en wat geschiedenis, maar geeft geen maandelijks overzicht per tarief dat zowel import als export bevat, en je kunt de ruwe data niet zelf bevragen. Met zonnepanelen op het dak wilde ik specifiek bijhouden hoeveel stroom er wordt teruggeleverd aan het net, uitgesplitst naar normaal- en daltarief, omdat dat direct invloed heeft op wat je betaalt onder de salderingsregeling.&lt;/p&gt;
&lt;p&gt;Wat ik wilde was simpel: een zelf-gehoste oplossing die op regelmatige intervallen metingen verzamelt, lokaal opslaat, en rapporten genereert die ik daadwerkelijk kan gebruiken. Geen cloudafhankelijkheid, geen abonnement, volledige controle over de data.&lt;/p&gt;
&lt;h2&gt;Hoe het werkt&lt;/h2&gt;
&lt;p&gt;Het systeem heeft twee hoofdcomponenten die in Docker draaien: een collector en een reporter.&lt;/p&gt;
&lt;p&gt;De collector bevraagt de HTTP API van de P1-meter op een instelbaar interval (standaard elke 60 seconden) en schrijft elke meting naar QuestDB, een tijdreeksdatabase die is geoptimaliseerd voor precies dit soort data: veel schrijfbewerkingen, chronologisch geordend. Elke meting bevat het huidige stroomverbruik, cumulatieve import- en exporttotalen opgesplitst per normaal- en daltarief, en gasverbruik. Voor huishoudens met zonnepanelen leggen de exportmetingen precies vast hoeveel stroom er per tarief wordt teruggeleverd aan het net.&lt;/p&gt;
&lt;p&gt;De reporter draait op een schema via APScheduler. Op de eerste van elke maand bevraagt hij de opgeslagen data, berekent het verbruik en de productie per tarief, bepaalt het nettoverbruik, en verstuurt een HTML-e-mail met een volledig overzicht. Elk maandrapport bevat automatisch een vergelijking met de vorige maand, met het verschil in elektriciteits- en gasverbruik zowel in absolute getallen als in percentages, kleurgecodeerd groen of rood zodat je in een oogopslag ziet of je meer of minder hebt verbruikt. Er is ook een optionele wekelijkse CSV-export voor wie zelf analyses wil doen in een spreadsheet.&lt;/p&gt;
&lt;h2&gt;Rapporten en queries&lt;/h2&gt;
&lt;p&gt;Het maandrapport wordt als HTML-e-mail verstuurd in het Nederlands, passend bij de tariefstructuur waarover het rapporteert (normaal- en daltarief). Het toont precies hoeveel kilowattuur er per tarief is verbruikt en teruggeleverd, het nettoverbruik na aftrek van zonnepaneelproductie, gasverbruik, en gemiddeld en piekvermogen. De vergelijking met de vorige maand laat zien of je elektriciteits- en gasverbruik omhoog of omlaag is gegaan, wat vooral nuttig is om seizoenspatronen te herkennen of het effect te zien van veranderingen zoals extra isolatie of andere verwarmingsgewoontes. Het idee is dat je een keer per maand een e-mail krijgt en direct weet waar je staat.&lt;/p&gt;
&lt;p&gt;Het rapport bevat ook twee grafieken: een dagelijks verbruiksoverzicht voor de periode met elektriciteit en gas als aparte lijnen, en een jaaroverzicht als staafdiagram dat de huidige maand in context plaatst naast eerdere maanden.&lt;/p&gt;
&lt;p&gt;Voor alles buiten het standaardrapport is de webconsole van QuestDB beschikbaar op het lokale netwerk. De ruwe data staat in een enkele &lt;code&gt;p1_meter_data&lt;/code&gt;-tabel, dus eigen queries draaien is eenvoudig. Wil je je dagverbruik over de afgelopen week weten, of zien hoeveel je zonnepanelen afgelopen dinsdag hebben opgewekt? Een paar regels SQL volstaan.&lt;/p&gt;
&lt;p&gt;Rapporten kunnen ook handmatig worden gegenereerd via de command line. Je kunt voorgedefinieerde periodes opgeven zoals &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;this-week&lt;/code&gt; of &lt;code&gt;yesterday&lt;/code&gt;, of een aangepast datumbereik meegeven. De CLI kan het rapport als e-mail versturen of naar een CSV-bestand schrijven.&lt;/p&gt;
&lt;h2&gt;Deployment&lt;/h2&gt;
&lt;p&gt;Alles draait in Docker Compose. Configuratie zit in een enkel &lt;code&gt;.env&lt;/code&gt;-bestand: de API-URL van de meter, SMTP-gegevens voor e-mailbezorging, en de ontvangers van de rapporten. Het systeem starten is een &lt;code&gt;docker compose up -d&lt;/code&gt; en het begint direct met verzamelen.&lt;/p&gt;
&lt;p&gt;Voor productiegebruik is er een apart Compose-bestand dat een kant-en-klare image uit het GitHub Container Registry haalt in plaats van lokaal te bouwen. Een CI/CD-workflow publiceert automatisch nieuwe images bij elke push. Backup- en restorscripts beheren het QuestDB-datavolume, zodat er niets verloren gaat als een container opnieuw moet worden aangemaakt.&lt;/p&gt;
&lt;h2&gt;Wat ik heb geleerd&lt;/h2&gt;
&lt;p&gt;Dit project bevestigde iets dat ik steeds opnieuw merk: de beste infrastructuur is het soort dat je vergeet dat het draait. De collector schrijft al maanden elke minuut metingen weg zonder dat ik er iets aan hoef te doen. De maandelijkse e-mail komt gewoon binnen. Ik gebruik het systeem alleen als ik daadwerkelijk de data wil bekijken.&lt;/p&gt;
&lt;p&gt;QuestDB bleek uitstekend te passen. Het is licht genoeg om naast de collector te draaien op bescheiden hardware, en het SQL-dialect ondersteunt tijdreeksaggregaties standaard. Samplen per dag, week of maand is een enkele clause, geen handmatige GROUP BY op losse datumdelen.&lt;/p&gt;
&lt;p&gt;Dat de e-mailtemplates in het Nederlands zijn, was een bewuste keuze. Energietarieven in Nederland volgen een specifieke structuur, en het rapport daaromheen bouwen maakte het direct bruikbaar in plaats van generiek. Het is eenvoudig genoeg om aan te passen voor andere talen of tariefmodellen, maar beginnen met iets concreets en specifieks bleek praktischer dan vanaf dag een universeel proberen te zijn.&lt;/p&gt;
&lt;p&gt;De repository is te vinden onderaan deze pagina, inclusief volledige installatie-instructies en voorbeeldqueries.&lt;/p&gt;
&lt;p&gt;Heb je feedback of vragen over dit project? Of ben je op zoek naar hulp bij softwareontwikkeling? &lt;a href=&quot;/nl/#contact&quot;&gt;Neem gerust contact op&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Python</category><category>Docker</category><category>QuestDB</category></item><item><title>TriviJava: Een Quiz App bouwen met Java</title><link>https://gerwinkuijntjes.nl/nl/projecten/een-quiz-app-bouwen-met-java/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/projecten/een-quiz-app-bouwen-met-java/</guid><description>Een eenvoudige quizapplicatie bouwen met Java Spring Boot, waarbij trivia-vragen van een externe API worden opgehaald en antwoorden veilig worden verwerkt zonder database.</description><pubDate>Fri, 06 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Inleiding&lt;/h2&gt;
&lt;p&gt;Het is tijd voor iets nieuws. Inmiddels heb ik genoeg ervaring met het bouwen van .NET API&apos;s dat ik het zo uit mijn mouw
kan schudden. Daarom ga ik mezelf uitdagen door het eens een keer op een andere manier te doen: met Java Sprint Boot.
Waarschijnlijk komt er nu een heleboel weerstand in je omhoog als je geen Java-developer bent, omdat online de consensus
is dat Java outdated is en geen goede Developer Experience biedt. Toch heb ik ook gelezen dat sommige Java Developers
juist zeggen dat ze niets anders willen. Daarom ben ik benieuwd hoe het nu daadwerkelijk is en dus ga ik het
uitproberen.&lt;/p&gt;
&lt;p&gt;Ik verwacht dat een groot deel overeenkomt met de traditionele manier waarop .NET het doet: controllers, services,
repositories en een database. Tenminste, als je de package by layer structuur volgt. Zelf geef ik de voorkeur aan de
package by feature structure (vertical slices), maar omdat ik wil ervaren hoe Sprint Boot in het bedrijfsleven wordt
gebruikt, doe ik het nu op de package by layer manier.&lt;/p&gt;
&lt;p&gt;Persoonlijk leer ik het snelst als ik zie hoe een deskundige het gebruikt en het kan relateren aan wat ik al weet. Dus om
een eerste indruk te krijgen van hoe Sprint Boot in elkaar zit, ging ik eerst maar eens
op zoek naar wat voorbeelden. Daarbij heb ik de
video &lt;a href=&quot;https://www.youtube.com/watch?v=7nonQ2dYgiE&quot;&gt;Spring Boot CRUD Tutorial: Building a Book Management Application&lt;/a&gt;
bekeken.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: versnel de video snelheid van YouTube tutorials om sneller te leren en spoel ook gerust door als het je al
bekend
voorkomt. Deze video bekeek ik op snelheid 3x, waardoor ik snel doorkreeg hoe een CRUD API gemaakt kan worden met
Sprint
Boot.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Daarna heb ik blog artikelen gelezen waarin de auteur de Java-technologieën vergelijkt met .NET-technologieën.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/bassem-hussein/getting-started-with-spring-boot-3-for-net-developers-5h46&quot;&gt;Getting Started with Spring Boot 3 for .NET Developers &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/bassem-hussein/getting-started-with-spring-boot-3-for-net-developers-part-2-building-a-product-entity-crud-69l&quot;&gt;Building a Product Entity CRUD Application in Spring Boot for .NET Developers &lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zodoende was ik in een kwartiertje op de hoogte. Tijd om de proef op de som te stellen...&lt;/p&gt;
&lt;p&gt;Wat ga ik eigenlijk bouwen? Ik kwam een opdracht tegen om een web app met API te bouwen waarin je een quiz kunt doen met
Trivia
vragen.&lt;/p&gt;
&lt;h2&gt;Opdracht&lt;/h2&gt;
&lt;p&gt;Ontwikkel een eenvoudige webapplicatie waarmee gebruikers trivia-vragen kunnen beantwoorden. De vragen worden opgehaald
via de Open Trivia Database API (OpenTDB API), die antwoorden in JSON-formaat retourneert.&lt;/p&gt;
&lt;p&gt;Belangrijke aandachtspunten:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;De ruwe API-respons bevat zowel de vraag als het correcte antwoord, wat betekent dat een gebruiker met technische
kennis gemakkelijk de juiste antwoorden kan achterhalen.&lt;/li&gt;
&lt;li&gt;De applicatie moet zodanig worden opgezet dat deze informatie niet eenvoudig te achterhalen is via de
browserontwikkeltools.&lt;/li&gt;
&lt;li&gt;De interface moet simpel zijn en de gebruiker gemakkelijk door de quiz leiden.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vereisten:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gebruik van Java Sprint Boot voor de backend-implementatie.&lt;/li&gt;
&lt;li&gt;Ophalen van trivia-vragen van de externe Open Trivia Database API.&lt;/li&gt;
&lt;li&gt;Basis frontend om interactie met de gebruiker mogelijk te maken.&lt;/li&gt;
&lt;li&gt;Implementatie van logica om het correcte antwoord te verbergen totdat de gebruiker antwoordt.&lt;/li&gt;
&lt;li&gt;Een database is niet vereist&lt;/li&gt;
&lt;li&gt;Gehoste versie is optioneel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hoog tijd om aan de slag te gaan.&lt;/p&gt;
&lt;h2&gt;Voorbereiding&lt;/h2&gt;
&lt;p&gt;Eerst ga ik mezelf trakteren op nóg een JetBrains applicatie: IntelliJ IDEA. De IDE&apos;s van Jetbrains vind ik heerlijk
werken. Nadat deze geïnstalleerd is maak ik een nieuw project aan. Ik kies voor een nieuw Java Sprint Boot project met
Maven en met de Spring Web dependency. Het project krijgt de naam TriviJava (Trivia + Java). Erg origineel, I know.&lt;/p&gt;
&lt;p&gt;Wat heb ik eigenlijk allemaal nodig? Ik bedenk een ruw stappenplan:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Maak een controller met twee endpoints:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /api/v1/quiz/questions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /api/v1/quiz/answers&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Maak een QuizService met de volgende functionaliteiten:
&lt;ul&gt;
&lt;li&gt;Ophalen van Trivia vragen bij de externe API&lt;/li&gt;
&lt;li&gt;Controleren van gegeven antwoorden&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bouw een web app waarop de gebruiker een quiz kan starten, de vragen kan beantwoorden en antwoorden kan controleren.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Er mogen geen antwoorden naar de client worden gestuurd. Dat betekent dat de backend de gegeven antwoorden zal moeten
valideren. De externe API genereert random vragen. Dus om later te kunnen valideren of de gegeven antwoorden correct
zijn, moet in de backend worden opgeslagen wat het juiste antwoord is op de vraag. Aangezien een database geen vereiste
is, houd ik het voor nu simpelweg bij een Map&amp;#x3C;UUID, UUID&gt; waarbij de key een zelfgegenereerd questionId en de value een
zelfgegenereerd answerId is. Het
is in dit scenario niet nodig om andere dingen op te slaan. Anders zou je natuurlijk ook andere data in de Map op kunnen
slaan.&lt;/p&gt;
&lt;p&gt;Het lijkt allemaal makkelijk te doen, gewoon een standaard GET request met DTOs, een controller, een service, een
repository en een ApiClient. Maar toen opende ik de website van OpenTDB. Daar bleek dat de OpenTDB API een geheel eigen
conventie gebruikt met tokens en response_codes en een rate limiting heeft van 1 request per 5 seconden. Dat was meteen
weer een les voor mij om me aan de HTTP en REST standaarden te houden. Enfin, het werd dus iets meer werk doordat moest
worden omgegaan met token management, de verschillende response codes en sterke rate limiting.&lt;/p&gt;
&lt;p&gt;Een voorbeeld response:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;response_code&quot;: 0,
  &quot;results&quot;: [
    {
      &quot;type&quot;: &quot;multiple&quot;,
      &quot;difficulty&quot;: &quot;medium&quot;,
      &quot;category&quot;: &quot;Entertainment: Books&quot;,
      &quot;question&quot;: &quot;According to The Hitchhiker&amp;#x26;#039;s Guide to the Galaxy book, the answer to life, the universe and everything else is...&quot;,
      &quot;correct_answer&quot;: &quot;42&quot;,
      &quot;incorrect_answers&quot;: [
        &quot;Loving everyone around you&quot;,
        &quot;Chocolate&quot;,
        &quot;Death&quot;
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Je wilt als gebruiker natuurlijk niet steeds dezelfde vragen krijgen. Daarom heeft OpenTDB een tokensysteem opgezet. Je
kunt eerst een token ophalen door een request te doen. Vervolgens kun je deze token steeds meesturen en krijg je
gegarandeerd unieke vragen, totdat alle vragen (&gt;20.000) aan de beurt geweest zijn. Op dat moment krijg je een bepaalde
response code
en kun je de token weer resetten. Na 6 uur inactiviteit, wordt de token ongeldig. Dan kan het dus zijn dat je ook weer
een bepaalde response code terugkrijgt, waarna je een nieuwe token moet ophalen. Ook geldt een zware rate limiting van 1
request per 5 seconden. De TriviaApiClient moet hier dus allemaal netjes mee omgaan.&lt;/p&gt;
&lt;h2&gt;API&lt;/h2&gt;
&lt;p&gt;Ik begin eerst maar met de controller. Er zijn dus twee endpoints nodig.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController()
@RequestMapping(&quot;api/v1/quiz&quot;)
public class QuizController {

    private final QuestionService questionService;

    public QuizController(QuestionService questionService) {
        this.questionService = questionService;
    }

    @GetMapping(&quot;questions&quot;)
    public ResponseEntity&amp;#x3C;ListQuestionResponse&gt; listQuestions(
            @RequestHeader(&quot;X-Session-Id&quot;) @NotEmpty String sessionId,
            @RequestParam(defaultValue = &quot;10&quot;) @Min(1) @Max(50) int amount
    ) {
        var questions = questionService.getQuestions(sessionId, amount);
        var response = ListQuestionResponse.fromDomain(sessionId, questions);
        return ResponseEntity.ok(response);
    }

    @PostMapping(&quot;answers&quot;)
    public ResponseEntity&amp;#x3C;CheckAnswersResponse&gt; checkAnswers(@RequestBody CheckAnswersRequest request) {
        var answeredQuestions = request.toDomain();
        var results = questionService.checkAnswers(answeredQuestions);
        var response = CheckAnswersResponse.fromDomain(results);
        return ResponseEntity.ok(response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sprint Boot heeft allerlei annotaties die je kunt toewijzen aan bijvoorbeeld een class of method. Zo wordt
&lt;code&gt;@RestController&lt;/code&gt; gebruikt om aan te geven dat het een controller is met REST endpoints. &lt;code&gt;@RequestMapping&lt;/code&gt; kan worden
gebruikt om een route toe te wijzen aan de gehele class. Met &lt;code&gt;@GetMapping&lt;/code&gt; kan een route worden aangegeven waarnaar een
GET request kan worden gedaan. Hetzelfde geldt voor andere HTTP methoden. Ook zijn er allerlei gemakkelijke notaties om
aan te geven waar de endpoint &apos;variabelen&apos; vandaan moet halen, zoals een header, body of query param. Validatie kan ook
aangegeven worden.&lt;/p&gt;
&lt;p&gt;Wat ik ook heel mooi vind aan Sprint Boot is het Dependency Injection systeem om het IoC-principe toe te passen. In .NET
was ik gewend dit zelf te moeten toevoegen aan de DI-container, maar Sprint Boot doet dat wederom met annotaties. Heel
convenient.&lt;/p&gt;
&lt;p&gt;In dit geval zijn er dus twee endpoints, die beiden een ResponseEntity returnen. Om vragen op te halen is een OpenTDB
token nodig. Elke gebruiker zal al andere vragen hebben gehad, dus is het wel zo handig om bij te houden welke token bij
de gebruiker hoort. Dit is gedaan door middel van een X-Session-Id. De front-end zal deze mee moeten sturen om de
gebruiker te identificeren. In de &lt;code&gt;TokenRepository&lt;/code&gt; wordt deze gekoppeld aan een OpenTDB token.&lt;/p&gt;
&lt;p&gt;Persoonlijk vind ik dat een controller maar weinig dingen mag doen: request validatie, DTO mapping, logica aansturen en
response met resultaat
terugsturen. Andere logica hoort daar niet thuis. Daarom heb ik zowel request als response DTOs gemaakt. Elke request
DTO heeft
methoden om zich om te zetten naar domain models. Deze kunnen naar een service worden gestuurd om iets mee te doen.
Nadat dit is gebeurd, wordt het resultaat omgezet in een response DTO en weer teruggestuurd. Op deze manier blijft de
controller overzichtelijk en is de logica goed gescheiden.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public record CheckAnswersRequest(@NotEmpty List&amp;#x3C;Answer&gt; answers) {
    record Answer(UUID questionId, UUID answerId) {}

    public List&amp;#x3C;AnsweredQuestion&gt; toDomain() {
        return answers.stream()
                .map(a -&gt; new AnsweredQuestion(a.questionId(), a.answerId()))
                .toList();
    }

    public static CheckAnswersRequest fromDomain(List&amp;#x3C;AnsweredQuestion&gt; answeredQuestions) {
        var answers = answeredQuestions.stream()
                .map(q -&gt; new Answer(q.questionId(), q.answerId()))
                .toList();
        return new CheckAnswersRequest(answers);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Voor DTOs gebruik ik records, vanwege immutability en automatisch gegenereerde methoden voor de fields. Ik was verbaasd
toen ik zag dat deze bestonden (in .NET ben ik ze namelijk gewend). Door allerlei reacties online te lezen,
had ik namelijk nogal een outdated indruk gekregen van Java. Maar hoe meer ik bezig was met Java, hoe mooier ik de taal
begon te vinden.&lt;/p&gt;
&lt;p&gt;De logica voor het verkrijgen van de vragen en checken van de antwoorden bevindt zich in de &lt;code&gt;QuestionService&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class QuestionService {

    private static final Logger logger = LoggerFactory.getLogger(QuestionService.class);
    private final QuestionRepository questionRepository;
    private final TokenRepository tokenRepository;
    private final TriviaApiClient triviaClient;

    public QuestionService(QuestionRepository questionRepository, TokenRepository tokenRepository, TriviaApiClient triviaClient) {
        this.questionRepository = questionRepository;
        this.tokenRepository = tokenRepository;
        this.triviaClient = triviaClient;
    }

    public List&amp;#x3C;Question&gt; getQuestions(String sessionId, int amount) {
        var token = tokenRepository.findToken(sessionId)
                .orElseGet(() -&gt; {
                    var newToken = triviaClient.requestNewToken();
                    tokenRepository.saveToken(sessionId, newToken);
                    return newToken;
                });

        try {
            return getQuestionsAndSaveCorrectAnswer(token, amount);
        } catch (TokenNotFoundTriviaApiException ex) {
            logger.warn(&quot;Token not found. Requesting new token...&quot;);
            var newToken = triviaClient.requestNewToken();
            tokenRepository.saveToken(sessionId, newToken);
            return getQuestionsAndSaveCorrectAnswer(newToken, amount);
        } catch (TokenExhaustedTriviaApiException ex) {
            logger.info(&quot;Token exhausted. Resetting token...&quot;);
            var resetToken = triviaClient.resetToken(token);
            return getQuestionsAndSaveCorrectAnswer(resetToken, amount);
        }
    }

    public List&amp;#x3C;Question&gt; getQuestionsAndSaveCorrectAnswer(String token, int amount) {
        var questions = triviaClient.fetchQuestions(token, amount);
        questions.forEach(question -&gt; questionRepository.storeCorrectAnswer(question.id(), question.correctAnswer().id()));
        return questions;
    }

    public List&amp;#x3C;AnswerResult&gt; checkAnswers(List&amp;#x3C;AnsweredQuestion&gt; answeredQuestions) {
        return answeredQuestions.stream()
                .map(q -&gt; {
                    var correctId = questionRepository.getCorrectAnswer(q.questionId());
                    return correctId.map(uuid -&gt; new AnswerResult(q.questionId(), q.answerId(), uuid)).orElse(null);
                })
                .filter(Objects::nonNull)
                .toList();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zoals gezegd wordt voor het ophalen van de vragen een bestaande token gebruikt of eerst een token opgehaald, waarbij een
verlopen token of invalide token wordt afgevangen. De interactie met de OpenTDB API gebeurt via de &lt;code&gt;TriviaApiClient&lt;/code&gt;.
Wat ik hier opmerkte, is dat Java Optional types heeft. Dat vind ik een mooie feature. Bij C# kun je een vraagteken
achter je object type zetten (bijv. &lt;code&gt;string? Example&lt;/code&gt;) om nullability aan te geven. Dat voelt alsof het er maar even als
lijm is bij geplakt. Een Optional type met allerlei handige methoden erbij, zoals &lt;code&gt;orElseGet&lt;/code&gt; voelt als een complete
feature (al is er een &lt;a href=&quot;https://github.com/dotnet/csharplang/blob/main/proposals/TypeUnions.md&quot;&gt;proposal&lt;/a&gt; voor C# om
Discriminated Unions te implementeren).&lt;/p&gt;
&lt;p&gt;Na het ophalen van de vragen wordt de &lt;code&gt;answerId&lt;/code&gt; van het correcte antwoord gekoppeld aan de &lt;code&gt;questionId&lt;/code&gt; in de
&lt;code&gt;questionRepository&lt;/code&gt; (voor nu in een Map).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Repository
public class QuestionRepositoryImpl implements QuestionRepository {

    private final Map&amp;#x3C;UUID, UUID&gt; correctAnswersByQuestionId = new HashMap&amp;#x3C;&gt;();

    @Override
    public void storeCorrectAnswer(UUID questionId, UUID correctAnswerId) {
        correctAnswersByQuestionId.put(questionId, correctAnswerId);
    }

    @Override
    public Optional&amp;#x3C;UUID&gt; getCorrectAnswer(UUID questionId) {
        return Optional.ofNullable(correctAnswersByQuestionId.get(questionId));
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Het checken van de antwoorden wordt gedaan door voor elk gegeven antwoord de &lt;code&gt;answerId&lt;/code&gt; te vergelijken met de opgeslagen
&lt;code&gt;answerId&lt;/code&gt; bij de betreffende &lt;code&gt;questionId&lt;/code&gt; in de &lt;code&gt;QuestionRepository&lt;/code&gt;. Daarbij heb ik gebruik gemaakt van
&apos;functional&apos; programmeren, door een stream te mappen en filteren. Deze manier van programmeren vind ik persoonlijk
heel erg overzichtelijk en duidelijk. De methodes geven aan wat je met de data doet, in tegenstelling tot loops waar
iedereen zijn eigen structuur aan kan geven. Ik ben dan ook een groot fan van LINQ (method syntax) in C#. Supertof dat
Java iets vergelijkbaars ondersteunt.&lt;/p&gt;
&lt;p&gt;De API functioneert nu. Er is een aantal verbeteringen mogelijk, zoals een global exception handler configureren en rate
limiting toevoegen, maar daar ga ik me nu even niet mee bezig houden. Wel heb ik unit tests geschreven en ook dat werkte
vlekkeloos.&lt;/p&gt;
&lt;h2&gt;Web&lt;/h2&gt;
&lt;p&gt;Om een quiz te maken, is een eenvoudige webapplicatie nodig. Aangezien dit een kleine opdracht is, kan het voor nu prima
met pure HTML, CSS en JS, zonder front-end frameworks. Om het simpel te houden voeg ik
&lt;code&gt;src/main/resources/static/index.html&lt;/code&gt;
toe aan het project. Sprint Boot maakt deze dan automatisch beschikbaar op &quot;{base_url}/path&quot;. &lt;code&gt;index.html&lt;/code&gt; is een
speciale naam, die automatisch wordt geladen, ook zonder path. Dus deze is bereikbaar op &lt;code&gt;http://localhost:8080&lt;/code&gt; (als je
het project hebt runnen natuurlijk ;)).&lt;/p&gt;
&lt;p&gt;Als je de pagina laadt, krijg je een laadanimatie te zien terwijl de vragen worden opgehaald. Daarna zie je de eerste
vraag waarop je een antwoord kunt selecteren.
&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/question.6fqejpGP.png&quot; alt=&quot;eerste vraag&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Een vraagscherm met vier antwoordopties.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Als je alle vragen hebt beantwoord, kun je de resultaten insturen en de antwoorden bekijken.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/result.HTifXtll.png&quot; alt=&quot;resultaat&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Het resultatenscherm na het voltooien van de quiz.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Daarna kun je een nieuwe quiz starten. Er wordt een unieke X-Session-Id opgeslagen in localstorage. Deze wordt
meegegeven aan de API om unieke vragen te genereren.&lt;/p&gt;
&lt;h2&gt;Deployment&lt;/h2&gt;
&lt;p&gt;Natuurlijk is een project niet echt af, als er geen deployment heeft plaatsgevonden waar dat kan. Ik ben ook benieuwd
hoe het dockerizen van een Sprint Boot applicatie werkt. Daarom ga ik de API als docker container deployen in mijn
homelab.&lt;/p&gt;
&lt;p&gt;Tijdens mijn zoektocht naar hoe dit moest, kwam ik tegen dat Sprint Boot via een Maven dependency support biedt voor het
starten van Docker containers. Dat lijkt op waar Microsoft recent mee bezig is: .NET Aspire. Dat is interessant om te
weten. Verder ben ik te weten gekomen dat de Sprint Boot applicatie kan worden compiled naar een native applicatie, met
GraalVM. Dat is een heel mooie feature (vergelijkbaar met AOT Compilation van Microsoft bij .NET). Wel verwacht ik
daarbij allerlei complicaties. Dus daarom kies ik nu voor een standaard Docker image. De Apache Maven Wrapper heeft
zelfs een command om een image te builden. Dat is ideaal. Aangezien ik geen automated CI/CD pipeline op ga zetten in
Github Actions, schrijf ik voor nu even een bash script &lt;code&gt;deploy.sh&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=ghcr.io/gwku/trivijava
echo $GITHUB_TOKEN | docker login ghcr.io -u gwku --password-stdin
docker push ghcr.io/gwku/trivijava:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Daarna geef ik run permissions aan het script en run ik deze: &lt;code&gt;chmod +x ./deploy.sh &amp;#x26;&amp;#x26; ./deploy.sh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Dan rest mij nog om een compose.yml file te maken en deze in mijn homelab te gebruiken. Ik maak gebruik van een proxy
manager, dus er hoeven geen ports open te staan.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  trivijava:
    image: ghcr.io/gwku/trivijava:latest
    networks:
      - proxy
    environment:
      SPRING_PROFILES_ACTIVE: &quot;prod&quot;
    restart: unless-stopped

networks:
  proxy:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En ja hoor, eventjes later draait de applicatie op
&lt;a href=&quot;https://trivijava.gkloud.nl&quot;&gt;https://trivijava.gkloud.nl&lt;/a&gt;. Nou kan ik heerlijk achterover leunen en een quiz spelen. Missie geslaagd.&lt;/p&gt;
&lt;h2&gt;Reflectie&lt;/h2&gt;
&lt;p&gt;Wat me tijdens deze opdracht echt positief heeft verrast, is hoe makkelijk en prettig het was om een Spring Boot API te
bouwen. Als iemand met ervaring in .NET (en informatica-opleiding-ervaring met Java 8) was ik een beetje sceptisch, mede
door de online sfeer rondom Java. Vaak wordt Java neergezet als verouderd, log of omslachtig. Maar eerlijk? Die
vooroordelen bleken grotendeels ongegrond. Ik vond het juist elegant, goed gedocumenteerd en dankzij Spring Boot ook
behoorlijk intuïtief.&lt;/p&gt;
&lt;p&gt;De annotatie-gedreven aanpak, de automatische dependency injection, het gebruik van records, en de functional-style
features zoals streams en optionals... het voelde allemaal veel moderner dan ik had verwacht. De meeste features zijn
officieel
ondersteund en ingebouwd, in tegenstelling tot .NET, waar belangrijke packages,
zoals &lt;a href=&quot;https://github.com/MassTransit/MassTransit&quot;&gt;MassTransit&lt;/a&gt;, &lt;a href=&quot;https://github.com/jbogard/MediatR&quot;&gt;MediatR&lt;/a&gt; en
&lt;a href=&quot;https://github.com/fluentassertions/fluentassertions&quot;&gt;FluentAssertions&lt;/a&gt; zomaar hun LICENSE veranderen en geld gaan
vragen (recent gebeurd).&lt;/p&gt;
&lt;p&gt;De belangrijkste les die ik hieruit trek: laat je niet te veel leiden door de online consensus. Java is zeker niet dood.
Het is juist een
volwassen, krachtige taal met een rijk ecosysteem — als je het een kans geeft, zul je merken dat er veel moois in zit.&lt;/p&gt;
&lt;p&gt;Sterker nog, ik vond het zó interessant, dat ik na het bouwen van deze eenvoudige API ben gaan kijken naar wat er
allemaal meer mogelijk is met Spring. Tijdens mijn zoektocht kwam ik
een &lt;a href=&quot;https://www.youtube.com/watch?v=GAgelbsTb9M&quot;&gt;video&lt;/a&gt; tegen over Sprint Boot End-to-End en geavanceerde concepten
zoals distributed messaging. By the way, de presentator vliegt daar door de code. Indrukwekkend.
Spring is veel meer dan alleen een web
framework. Het is een volwaardige enterprise toolkit die schaalbaarheid, messaging, event-driven architecturen en nog
veel meer ondersteunt.&lt;/p&gt;
&lt;p&gt;En dat zet je aan het denken... Als ik dit soort vooroordelen had over Java — wat voor andere technologieën of ideeën
heb ik dan nog onbewust afgeschreven, simpelweg omdat de bubbel waarin ik leef iets anders roept?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Belangrijkste inzicht: de techwereld zit vol met echo chambers. En voor je het weet denk je dat iets “slecht” is,
terwijl je het zelf nooit hebt geprobeerd. Door open te staan voor iets nieuws, heb ik niet alleen een leuke nieuwe
tool
geleerd, maar ook een bredere blik ontwikkeld.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ik kijk ernaar uit om meer met Spring Boot te doen. Niet omdat het &quot;moet&quot;, maar omdat het me echt heeft geïnspireerd.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;De link naar de repository vind je onderaan de pagina.&lt;/p&gt;
&lt;p&gt;Heb je feedback of vragen over dit project? Of ben je op zoek naar hulp bij
softwareontwikkeling? &lt;a href=&quot;/nl/#contact&quot;&gt;Neem gerust contact op&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Java</category></item><item><title>SecShare: een open-source secret deel tool</title><link>https://gerwinkuijntjes.nl/nl/projecten/secshare-open-source-secret-deel-tool/</link><guid isPermaLink="true">https://gerwinkuijntjes.nl/nl/projecten/secshare-open-source-secret-deel-tool/</guid><description>Een project voor een klant die gevoelige informatie wilde delen</description><pubDate>Mon, 04 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introductie&lt;/h2&gt;
&lt;p&gt;Een klant heeft de behoefte geuit om een tool te realiseren waarmee zij zonder de betrokkenheid van derde partijen
gevoelige informatie kunnen gaan delen.&lt;/p&gt;
&lt;p&gt;Een gebruiker moet verschillende dingen met de tool kunnen doen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Als gebruiker wil ik met mijn wachtwoord een link kunnen aanmaken zodat ik deze kan delen.&lt;/li&gt;
&lt;li&gt;Als gebruiker wil ik een wachtwoord kunnen bekijken na het openen van een link zodat ik deze kan opslaan.&lt;/li&gt;
&lt;li&gt;Als gebruiker wil ik dat mijn wachtwoord wordt verwijderd na het openen van een link zodat de tool AVG compliant is.&lt;/li&gt;
&lt;li&gt;(optioneel) Als gebruiker wil ik een maximale geldigheidsduur van mijn link kunnen instellen zodat deze niet eeuwig
beschikbaar blijft.&lt;/li&gt;
&lt;li&gt;(optioneel) Als gebruiker wil ik het maximaal aantal uses van mijn link kunnen instellen zodat deze door meerdere
personen te gebruiken is.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;De oplossing moet gemaakt worden in Laravel en voor het UI moet Bootstrap of TailwindCSS gebruikt worden. Verder is het
belangrijk dat de oplossing is te gebruiken middels een UI op desktop, geen foutmeldingen vertoont bij normaal gebruik (
duh) en voldoende is gedocumenteerd in de README.&lt;/p&gt;
&lt;p&gt;En dan het belangrijkste: ik mocht me vrij voelen om de opdracht naar eigen inzicht aan te passen of uit te breiden waar
ik dat nodig vind. Dat waardeert het bedrijf juist. Dit versterkte het idee dat het een mooi bedrijf is met een goede
houding en een openheid voor ideeën. Daarom ben ik vastbesloten om hier een mooi project van te maken.&lt;/p&gt;
&lt;p&gt;Dus, aan de slag. Dat begint bij nadenken en voorbereiden.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;By failing to prepare, you&apos;re preparing to fail.&quot; — Benjamin Franklin&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Het belangrijkste component van deze tool is de veiligheid die het kan waarborgen. De klant gaf uitdrukkelijk aan dat
hij gevoelige informatie wil kunnen gaan delen en dat zonder betrokkenheid van derde partijen. Het moet dus mogelijk
zijn de gevoelige data te versleutelen.&lt;/p&gt;
&lt;h2&gt;Veiligheid&lt;/h2&gt;
&lt;p&gt;Er zijn verschillende technieken om data te versleutelen. Daarom moet bepaald worden wat de beste techniek is voor deze
tool.&lt;/p&gt;
&lt;h3&gt;Server-side vs client-side&lt;/h3&gt;
&lt;p&gt;Encryptie kan op de server gebeuren, door de data die binnenkomt te versleutelen en data die eruitgaat te ontsleutelen.
Dit heeft als nadeel dat de beheerder van de server en de database zelf ook alle data kan ontsleutelen. Dat vind ik niet
goed als het gaat om gevoelige data.&lt;/p&gt;
&lt;p&gt;Encryptie kan ook client-side gebeuren, dus in de browser. Op die manier krijgt de server alleen versleutelde data
binnen en kan niemand anders dan degene met de link deze ontsleutelen. Dat is dus veel veiliger. Daarom ga ik voor deze
optie.&lt;/p&gt;
&lt;p&gt;Er zijn twee soorten encryptie: asymmetrisch (met een private en public key pair per partij) en symmetrisch (met een
gedeelde sleutel). De eerste is veiliger (omdat de private key niet gedeeld hoeft te worden), maar vereist het aanmaken
van zo&apos;n sleutelpaar. Dat is niet handig als je een secret naar iemand wilt sturen. Daarom is symmetrische encryptie
hier de beste optie.&lt;/p&gt;
&lt;h3&gt;Sleutel delen met ontvanger&lt;/h3&gt;
&lt;p&gt;Daarmee is nog niet alles gezegd. Hoe wordt de sleutel gedeeld met de ontvanger? Als die in de link wordt meegestuurd,
krijgt de server de sleutel ook binnen en is er nog geen sprake van end-to-end encryptie.&lt;/p&gt;
&lt;h3&gt;Url hash&lt;/h3&gt;
&lt;p&gt;Voor dit probleem ging ik op de schouders van de reuzen achter mijn favoriete
tekentool &lt;a href=&quot;https://excalidraw.com&quot;&gt;Excalidraw&lt;/a&gt; staan.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;If I have seen further, it is by standing on the shoulders of giants.&quot; — Isaac Newton&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Excalidraw heeft op een geniale manier de &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL/hash&quot;&gt;url hash&lt;/a&gt; van de
browser gebruikt voor end-end-encryption. De url hash ziet er bijvoorbeeld zo uit &lt;code&gt;https://secshare.com/#id_of_element&lt;/code&gt;
en is bedoeld voor de browser om te navigeren naar het element met het id dat achter de &lt;code&gt;#&lt;/code&gt; staat. Deze url hash is een
client-side functie en wordt niet naar de server gestuurd. De browser haalt deze zelfs automatisch weg uit de url bij
een server request. Deze functie heeft volledige browser compatibility.&lt;/p&gt;
&lt;p&gt;Dit kan mooi gebruikt worden voor de tool, door eerst de data client-side te versleutelen en dan de sleutel in de url
hash te zetten om te delen met de ontvanger: &lt;code&gt;#key=reallysecurekeyhere&lt;/code&gt;. Op die manier krijgt de server alleen
versleutelde data zonder sleutel en krijgt de ontvanger wel de sleutel, om de versleutelde data van de server weer te
ontsleutelen.&lt;/p&gt;
&lt;h2&gt;Tech stack&lt;/h2&gt;
&lt;p&gt;De oplossing zal gebouwd worden met Laravel en een UI framework. De keuze tussen Bootstrap en Tailwind is niet zo
moeilijk: ik heb een hekel aan Bootstrap en een passie voor Tailwind.&lt;/p&gt;
&lt;p&gt;Laravel kan op verschillende manieren gebruikt worden:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server Side Rendered (SSR) met Blade of JS components&lt;/li&gt;
&lt;li&gt;API + front-end framework naar keuze (evt. met Inertia)&lt;/li&gt;
&lt;li&gt;Volt (class based of functional style)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ik vind het belangrijk om te kijken naar wat een oplossing vereist in plaats van de nieuwste, hipste en populairste
middelen te gebruiken. Hoewel ik weet dat het bedrijf waar ik solliciteer Nuxt gebruikt, kies ik ervoor om dat niet te
doen, omdat het simpelweg niet nodig is en dus overhead geeft.&lt;/p&gt;
&lt;p&gt;De tool is een simpele CRUD (Create, Read, Update, Delete), heeft alleen formulieren nodig, en vereist weinig
client-side interactie. Een SPA is dus absoluut niet nodig. Aangezien Blade mooie helper methods heeft, en er weinig
client-side interactie nodig is, zou het gebruik van JS components het ontwikkelproces alleen maar moeilijker maken dan
nodig. Voor een klein beetje interactie, zoals het zichtbaar en onzichtbaar maken van secrets, kan AlpineJS gebruikt
worden: een lichtgewicht Javascript library.&lt;/p&gt;
&lt;p&gt;De tool wordt dus gebouwd in Laravel met Blade, Tailwind en AlpineJS.&lt;/p&gt;
&lt;h2&gt;Installatie&lt;/h2&gt;
&lt;p&gt;Een naam voor het project laat niet lang op zich wachten: SecShare: een samenstelling van secure/secret en share. Niet
erg origineel, maar hé, ik kan het onthouden, het is kort genoeg om snel te typen en het geeft een aardig beeld van de
app :)&lt;/p&gt;
&lt;p&gt;Ik start een nieuw Laravel project zonder starterkit. Als database gebruik ik SQLite, omdat ik geen zin heb om een MySQL
instance aan te zetten (ze hebben mij geleerd dat developers lui moeten zijn). Alle environment variables zijn goed
ingesteld. Tijd voor modelling.&lt;/p&gt;
&lt;h2&gt;Modelling&lt;/h2&gt;
&lt;p&gt;Er is maar één model nodig: een &lt;code&gt;Secret&lt;/code&gt;. Aangezien geen authenticatie wordt gebruikt, kan het standaard User model en
de bijbehorende migratie worden verwijderd.&lt;/p&gt;
&lt;p&gt;Ik wil alle gevraagde functies implementeren en heb er zelf nog wat bij bedacht. Het kan zomaar voorkomen dat per
ongeluk een secret wordt gedeeld of de link in een verkeerd kanaal wordt gedeeld. Dan is het fijn als de link weer kan
worden ingetrokken. Die functionaliteit wil ik mogelijk maken.&lt;/p&gt;
&lt;p&gt;Het model &lt;code&gt;Secret&lt;/code&gt; heeft een aantal properties nodig:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: primary key (big integer)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content&lt;/code&gt;: versleutelde data&lt;/li&gt;
&lt;li&gt;&lt;code&gt;token&lt;/code&gt;: unieke random string die wordt gegenereerd voor de gedeelde link&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expires_at&lt;/code&gt;: moment waarop de link zijn geldigheid verliest&lt;/li&gt;
&lt;li&gt;&lt;code&gt;views&lt;/code&gt;: het aantal keer dat een link is bekeken&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_views&lt;/code&gt;: het aantal keer dat een link bekeken mag worden&lt;/li&gt;
&lt;li&gt;&lt;code&gt;revoke_token&lt;/code&gt;: random string om een link te verwijderen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Met deze eenvoudige setup is het mogelijk om SecShare te bouwen.&lt;/p&gt;
&lt;h2&gt;Functies&lt;/h2&gt;
&lt;p&gt;Er zijn vier verschillende functies die gebouwd moeten worden:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Aanmaken van secret (met max_views en expiration_date)&lt;/li&gt;
&lt;li&gt;Inzien van secret&lt;/li&gt;
&lt;li&gt;Intrekken van secret&lt;/li&gt;
&lt;li&gt;Opschonen van secrets&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.Aanmaken van secret&lt;/h3&gt;
&lt;p&gt;De gebruiker moet een link aan kunnen maken waarmee hij gevoelige data kan delen. Daarvoor moeten verschillende
invoervelden met validatie komen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;secret (met hidden/visible toggle)&lt;/li&gt;
&lt;li&gt;expires_in (met opties voor aantal uren)&lt;/li&gt;
&lt;li&gt;max_views (getal tussen 1 en 15)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Om end-to-end encryptie te implementeren, wordt op deze pagina een sleutel gegenereerd. Zodra de gebruiker op de submit
knop klikt, wordt de data versleuteld naar de server verzonden. De sleutel wordt opgeslagen in de localSessionStorage
van de browser voor later gebruik.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/secshare_create.D060wrHg.png&quot; alt=&quot;Screenshot of create page&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;De aanmaakpagina: voer het geheim in, stel een vervaldatum in en kies een maximaal aantal views.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;De invoer wordt gevalideerd aan de hand van de SecretStoreRequest, zodat de validatie gescheiden is van de logica en in
de flow ook altijd eerst wordt uitgevoerd, voordat de logica in de controller aan de beurt komt. Vergeten van validatie
is dan niet mogelijk.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function rules(): array  
{  
    return [  
        &apos;content&apos; =&gt; &apos;required&apos;,  
        &apos;expires_in&apos; =&gt; &apos;required|integer|in:1,12,24,48,72,168&apos;,  
        &apos;max_views&apos; =&gt; &apos;required|integer|min:1|max:15&apos;  
    ];  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nadat de input van de user is gevalideerd, moet een nieuwe Secret worden aangemaakt en opgeslagen in de database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function store(SecretStoreRequest $request)  
{  
    $validatedData = $request-&gt;validated();  
    $expiresAt = now(&apos;UTC&apos;)-&gt;addHours((int)$validatedData[&apos;expires_in&apos;]);  
    $token = Str::uuid();  
    $revoke_token = Str::random(15);  
  
    Secret::create([  
        ...$validatedData,  
        &apos;expires_at&apos; =&gt; $expiresAt,  
        &apos;token&apos; =&gt; $token,  
        &apos;revoke_token&apos; =&gt; password_hash($revoke_token, PASSWORD_DEFAULT),  
    ]);

    return view(&apos;secrets.link&apos;, [  
        &apos;link&apos; =&gt; url(&apos;/&apos;) . URL::temporarySignedRoute(&apos;secrets.show&apos;, $expiresAt, [&apos;secret&apos; =&gt; $token], false),  
        &apos;expires_in&apos; =&gt; $expiresAt-&gt;diffForHumans(),  
        &apos;max_views&apos; =&gt; $validatedData[&apos;max_views&apos;],  
        &apos;revoke_token&apos; =&gt; $revoke_token,  
    ]);}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Als het om datums gaat heb ik één regel: sla altijd alles op in UTC. Op die manier weet ik altijd dat ik de correcte
datum heb en kan ik deze eventueel omzetten. Hoewel mijn &lt;code&gt;TZ&lt;/code&gt; environment variable op UTC staat, lijkt &lt;code&gt;now()&lt;/code&gt; dat niet
te respecteren, dus geeft ik dat handmatig mee. Het aantal uren dat is gekozen wordt erbij opgeteld.&lt;/p&gt;
&lt;p&gt;De token die wordt gegenereerd is een UUID om te garanderen dat deze uniek is. Er hoeft niet te worden gevreesd voor
brute forcing, aangezien een hacker niets heeft aan de link zonder dat hij ook de sleutel heeft uit de url hash. Hij
heeft dan alleen nog maar versleutelde data.&lt;/p&gt;
&lt;p&gt;De revoke_token krijgt ook een random string toegewezen, zodat de eigenaar van de link zijn link kan intrekken als hij
dat wilt. Deze wordt opgeslagen als hash in de database, zodat een kwaadaardige hacker die toegang krijgt tot de
database (met read-only access), de links niet zelf kan verwijderen.&lt;/p&gt;
&lt;p&gt;De link wordt gegenereerd met behulp van Laravels &lt;a href=&quot;https://laravel.com/docs/11.x/urls#signed-urls&quot;&gt;signed route&lt;/a&gt;. Deze
wordt automatisch ongeldig als de expires_at datum is bereikt.&lt;/p&gt;
&lt;p&gt;Na het opslaan, wordt de gemaakte secret meegegeven aan de link page waar naartoe verwezen wordt.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/secshare_link.DUutAT4p.png&quot; alt=&quot;Screenshot of link page&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;De linkpagina: de deelbare URL met de encryptiesleutel als hash-fragment.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Deze link pagina haalt de opgeslagen sleutel uit de localSessionStorage op, en plakt deze achter de gegenereerde link
als hash. Bijvoorbeeld:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://secshare.com/secrets/4642bb947-b4a4-4290-9309-c7c2e6244873?expires=1730585002&amp;#x26;signature=b4007eaecc2c626e7f60556831d17c33feccbe3bdec79296d4acec5d67c3b4d4#key=C9k-8hW6P-c_FeEZxxEsvg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Er is nog wat informatie over de link te zien, zoals wanneer deze verloopt en hoe vaak deze bekeken kan worden. Ook de
revoke token kan hier verkregen worden.&lt;/p&gt;
&lt;h3&gt;2. Inzien van secret&lt;/h3&gt;
&lt;p&gt;Als de ontvanger de link opent, moet hij de gevoelige data kunnen zien. Dit geldt alleen als de link geldig is. Dus dat
moet eerst gecontroleerd worden.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function show(Secret $secret)  
{  
    abort_unless(request()-&gt;hasValidRelativeSignature(), 403);  
  
    $secret-&gt;increment(&apos;views&apos;);  
  
    if ($secret-&gt;views &gt;= $secret-&gt;max_views) {  
        $secret-&gt;delete();  
    }
    
    return view(&apos;secrets.show&apos;, [  
        &apos;secret&apos; =&gt; $secret-&gt;content,  
        &apos;expires_in&apos; =&gt; $secret-&gt;expires_at-&gt;diffForHumans(),  
        &apos;views&apos; =&gt; $secret-&gt;max_views - $secret-&gt;views,  
        &apos;token&apos; =&gt; $secret-&gt;token,  
    ]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Als de url niet geldig is of de secret is verlopen, krijgt de gebruiker een 403 Forbidden. Als de gebruiker wel toegang
heeft tot de data, wordt het aantal views opgehoogd en krijgt hij de show pagina te zien.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/secshare_show.Ba_G7AuA.png&quot; alt=&quot;Screenshot of show page&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;De weergavepagina: de ontvanger bekijkt en kopieert het ontsleutelde geheim.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Daar kan hij de data inzien en gemakkelijk kopiëren. Verder ziet hij informatie zoals expires_in en het aantal keer dat
de secret nog mag worden bekeken. Ook is er de optie om de secret in te trekken.&lt;/p&gt;
&lt;h3&gt;3. Intrekken van secret&lt;/h3&gt;
&lt;p&gt;Als een gebruiker de secret wil intrekken, zal hij de juiste token in moeten voeren, die hij kreeg tijdens het aanmaken
van de link.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://gerwinkuijntjes.nl/_astro/secshare_revoke.zZ_G9eu_.png&quot; alt=&quot;Screenshot of revoke page&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;De intrekkingspagina: voer het revoke-token in om het geheim definitief te verwijderen.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Als de hash daarvan overeenkomt met die in de database, zal de link worden verwijderd.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function destroy(Secret $secret, SecretDeleteRequest $request)  
{  
    if (!password_verify($request-&gt;revoke_token, $secret-&gt;revoke_token)) {  
        return back()-&gt;withErrors([&apos;revoke_token&apos; =&gt; &apos;The provided token is incorrect.&apos;]);  
    }
    
    $secret-&gt;delete();  
    
    return redirect()-&gt;route(&apos;secrets.create&apos;)-&gt;with(&apos;success&apos;, &apos;Secret deleted successfully!&apos;);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;De gebruiker krijgt hiervan feedback door middel van een succes bericht.&lt;/p&gt;
&lt;h3&gt;4. Opschonen van secrets&lt;/h3&gt;
&lt;p&gt;Zodra een secret zijn maximaal aantal views heeft bereikt, wordt deze verwijderd. Dit geldt echter niet als de secret is
verlopen, omdat wordt gecheckt of de url nog wel geldig is in de controller. Geldigheid wordt niet alleen gebaseerd op
de expiration date, maar ook op de signature. Als een gebruiker een verkeerde signature stuurt, moet de Secret
natuurlijk niet worden verwijderd.&lt;/p&gt;
&lt;p&gt;Daarom wordt een command gemaakt: &lt;code&gt;RemoveExpiredSecrets&lt;/code&gt;. Dit command verwijdert alle secrets die verlopen zijn.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;protected $signature = &apos;secrets:remove-expired&apos;;  
protected $description = &apos;Remove expired secrets from the database&apos;;  
  
public function handle(): void  
{  
    $count = Secret::where(&apos;expires_at&apos;, &apos;&amp;#x3C;&apos;, Carbon::now())-&gt;delete();  
  
    if ($count === 0) {  
        $this-&gt;info(&apos;No expired secrets found.&apos;);  
    } else {  
        $this-&gt;info(&quot;Deleted {$count} expired secrets successfully.&quot;);  
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Om dit command regelmatig uit te voeren, wordt deze geregistreerd in &lt;code&gt;routes/console.php&lt;/code&gt;, waarna het gemakkelijk in een
cron job kan worden uitgevoerd. Op die manier blijft de database vrij van onnodige data en zijn de gebruikers ervan
verzekerd dat hun secrets worden verwijderd als deze ongeldig zijn geworden.&lt;/p&gt;
&lt;h2&gt;Landing page&lt;/h2&gt;
&lt;p&gt;Dit is een tool die ik zelf best zou kunnen gebruiken, dus wellicht zijn er meer mensen die dat willen. Daarom heb ik
besloten dit project open-source te maken. Om duidelijk te maken wat de tool inhoudt, heb ik een landing page gemaakt,
inclusief feature beschrijvingen en een FAQ.&lt;/p&gt;
&lt;p&gt;Zelf heb ik een homelab en aangezien deze tool ook geschikt is voor self-hosting, heb ik ook een Dockerfile en Docker compose file toegevoegd.
Zie de &lt;a href=&quot;https://github.com/gwku/secshare&quot;&gt;README&lt;/a&gt; op Github voor installatie-instructies.&lt;/p&gt;
&lt;h2&gt;Opmerkingen&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Verschillende methoden in de SecretController maken gebruik van Laravels Route Model Binding aan de hand van de token.
Dit is geconfigureerd met &lt;code&gt;getRouteKeyName&lt;/code&gt; in het &lt;code&gt;Secret&lt;/code&gt; model.&lt;/li&gt;
&lt;li&gt;Alle teksten die in de tool zijn gebruikt, worden getoond met Laravels localization features en kunnen gemakkelijk
worden vertaald in de toekomst.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tests&lt;/h2&gt;
&lt;p&gt;Om de kwaliteit en veiligheid van de tool te waarborgen, zijn er verschillende integratie tests geschreven (met Dusk).
Zo wordt gecontroleerd of de secret correct wordt aangemaakt, of de secret correct wordt ingetrokken, of de secret
onbereikbaar is bij een ongeldige url signature en of de secret correct wordt verwijderd na het bereiken van het
maximaal aantal views. Op deze manier kun je met een gerust hart gevoelige informatie delen en dit project refactoren of
aanpassen.&lt;/p&gt;
&lt;h2&gt;Reflectie&lt;/h2&gt;
&lt;p&gt;Al met al was het een leuke opdracht om een tool te maken voor een klant die gevoelige informatie wilde delen. Er moest
goed worden nagedacht over veiligheid. Ik heb twee nieuwe dingen geleerd tijdens dit project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;End-to-end encryptie door middel van een url hash&lt;/li&gt;
&lt;li&gt;Laravel Signed Routes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ik vond het een mooi project en de moeite waard. De code en een demo kun je onderaan de pagina vinden.&lt;/p&gt;
&lt;p&gt;Heb jij feedback, vragen of opmerkingen? Of heb je iemand nodig om een app of andere software te bouwen? Neem
gerust &lt;a href=&quot;/nl/#contact&quot;&gt;contact&lt;/a&gt; op!&lt;/p&gt;</content:encoded><category>Laravel</category><category>Tailwind</category><category>AlpineJS</category></item></channel></rss>