CLAUDE.md 180 KB

Uitgaanskrant — werkinstructies voor Claude

Werkdirectory

Werk uitsluitend binnen deze projectdirectory (/home/bob/Projects/ff-app/uitgaanskrant-1qhvtd). Niet daarbuiten zoeken of scannen — scope alle bestandsoperaties tot dit project.

Werkwijze binnen een sessie

  • Meld bij elke nieuwe stap kort vooraf wat je gaat doen, vóór je begint (staande voorkeur Bob, 2026-08-04).
  • Bob bespreekt elke taak in een nieuwe, aparte chat — geen eerdere conversatie om op terug te vallen. CLAUDE.md + TASKS.md zijn samen het volledige geheugen tussen sessies.
  • Groepeer opgepakte taken per sessie op FlutterFlow-paginagebied waar mogelijk (bv. twee taken op dezelfde pagina/component samen oppakken) — minder heen-en-weer-navigeren in de builder, minder tokens.
  • TASKS.md-status kan achterlopen op de echte code (Bob werkt gelijktijdig, en documentatie-updates lopen niet altijd synchroon met de export). Check een taak die er al even staat met git log --oneline -S"<kenmerkende string/tekst>" of een gerichte grep vóórdat je 'm oppakt — niet blind vertrouwen dat "open" betekent "nog niet gefixt". (Precedent: 2026-08-04 bleken 2 "open" P0-taken al op 2026-08-02 gefixt te zijn, git-bevestigd.)
  • Elke taak in TASKS.md heeft een stabiel ID (bv. P0-1) en een Eigenaar:-regelBob (sneller/simpeler voor hem zelf, meestal builder-UI met een bekend fragiele dialoog, zie hieronder), Claude (onbeklaimd, vrij op te pakken), of ... — bezig (iemand is er nu actief mee bezig). Vuistregel voor wie een nieuwe taak zou moeten doen: een kort, mechanisch herhaald patroon zonder geneste dialogen → Claude; ConditionalBuilder/JSON-Path-condities, List-typed function-argumenten, of iets dat eerder al vastliep → Bob.
  • ⚠️ Concurrency: Bob start elke taak in een nieuwe chat, dus er kunnen meerdere sessies tegelijk actief zijn. Check vóór je een taak oppakt of de Eigenaar-regel al "— bezig" zegt door iemand anders — zo ja, niet zelfstandig ook gaan bouwen aan hetzelfde bestand/component, vraag Bob eerst wie 'm afmaakt. Zet zelf "— bezig" zodra je serieus begint — dit geldt óók voor puur onderzoek/ reproductie (bv. een eigen flutter run-sessie starten om een stack trace te vangen), niet alleen voor builder-edits. (Precedent 2026-08-04: twee sessies pakten onafhankelijk dezelfde P0-taak op — Bob moest scheidsrechteren tussen twee stukken werk aan hetzelfde bestand. Tweede precedent, 2026-08-09: een sessie deed 20+ minuten actief P1-13-onderzoek — eigen flutter run-proces + een nieuwe bug gevonden (P1-19) — zonder ooit "— bezig" te zetten; een nieuwe sessie kwam er via git diff/proces-inspectie toevallig achter vlak vóórdat ze zelf hetzelfde pad zou inslaan. Geen schade, maar puur geluk — vandaar nu ook ff-session-check.sh, zie hieronder. Derde precedent, 2026-08-21: tijdens P1-26 (gemeente- hartje op HeaderButtonsComponent + het bijbehorende Drupal- endpoint) bleek een andere sessie al actief te bouwen — pas ontdekt via een onverwachte git status-diff (een halfklare print('IconButton pressed ...')-placeholder in de working tree), niet via een "— bezig"-marker in TASKS.md, want die was ook nu weer nooit gezet. Geen schade (beide sessies' werk sloot uiteindelijk netjes op elkaar aan), maar het bevestigt dat de regel hierboven in de praktijk niet gevolgd wordt.**
  • ⚠️ Scherpere regel (2026-08-21, Bob expliciet): "— bezig" zetten is geen mentale notitie voor bij het afsluiten — het is een directe Edit naar TASKS.md op schijf, uitgevoerd zodra je een taak serieus oppakt (vóór je in de builder/code begint), niet pas bij de afsluitroutine aan het eind van de sessie. Sessies delen dezelfde working directory op Bob's machine, dus een geschreven bestandswijziging is voor een andere sessie direct zichtbaar zonder dat er gecommit hoeft te worden — een aantekening die pas bij sessie-einde geschreven wordt, is voor niemand anders zichtbaar precies op het moment dat het er het meest toe doet. Symmetrische plicht bij het oppakken van een taak: check ook zelf eerst — vóór je begint — of de Eigenaar-regel al "— bezig" zegt (verse ff-session-check.sh of een gerichte grep op de taak-ID/component- naam), niet er blind van uitgaan dat de lijst nog klopt sinds je 'm voor het laatst las.
  • Start elke sessie met /home/bob/Projects/ff-session-check.sh (géén argumenten nodig voor dit project) — bundelt in één keer git-status, welke devices/emulators draaien, welke flutter/ff-run-fvm/script-processen actief zijn (met leeftijd), en welke TASKS.md-taken al "— bezig" staan. Vervangt de losse handmatige git status/adb devices/ps aux-stappen hieronder — dat was voorheen makkelijk (deels) over te slaan, dit script niet. Een draaiend ff-run-fvm.sh/flutter run-proces alléén is géén betrouwbaar signaal dat er "nu" iemand actief mee bezig is — bevestigd 2026-08-07: meerdere van die processen bleken dagen oud (gestart Aug05/Aug06, nog steeds draaiend), puur omdat het interactieve hot-restart-loop-karakter ze nooit vanzelf afsluit. Het betrouwbaardere signaal voor "hier ligt nog niet-afgerond werk": een niet-gecommitte git status/git diff (vooral als TASKS.md/ CLAUDE.md zelf ook wijzigingen tonen t.o.v. de laatste commit) — dát is een aanwijzing dat een sessie iets heeft afgerond/uitgevoerd maar de afsluitroutine (commit+push, zie hieronder) nog niet heeft gedaan. Beide signalen samen (proces + ongecommitte diff) zijn het sterkste bewijs van live werk — het script hierboven toont ze gecombineerd. Zie zo'n situatie: bouw erop voort waar zinnig, maar commit niet over andermans nog-niet-afgeronde wijziging heen zonder navraag.

Live pair-fix sessie — Bob doet de builder-klikken, Claude regisseert

Bevestigd goed werkende aanpak (2026-08-10 avond) voor het gezamenlijk afwerken van TASKS.md: Bob voert alle builder-wijzigingen zelf uit in zijn eigen browser (geen clipping-/coördinaatproblemen daar), Claude geeft per taak de exacte stappen en verifieert elke taak na afloop met een verse, losse export in plaats van op de builder-UI ("Synced", geen foutmelding) te vertrouwen. In één avond leverde dit 9 afgeronde taken op tegen een prettig tempo, en ving het minstens 2x een edit die in de UI geslaagd leek maar bij export niet was doorgezet (zelfde stille-niet-opgeslagen-patroon als elders in dit bestand, nu ook bevestigd op een gewone Visibility-conditie, niet alleen bij de bekende clipping-gevallen).

Vaste aanpak per taak:

  1. Claude geeft exacte builder-stappen voor precies één taak uit TASKS.md, met taak-ID erbij (bv. "P1-3").
  2. Bob voert uit in eigen browser, meldt "gedaan".
  3. Claude verifieert met een losse, snelle export (geen emulator/build nodig):

    export PATH="/home/bob/fvm/bin:$HOME/.pub-cache/bin:$PATH"
    flutterflow export-code --project uitgaanskrant-1qhvtd \
     --dest /tmp/ff-check --token "$(cat ~/.ff_token)" \
     --no-parent-folder --as-debug
    

    gevolgd door een gerichte grep/Read op de betreffende regel(s). Dit duurt seconden en is niet-destructief (los van de projectmap, geen commit, geen emulator).

  4. Klopt het: taak-ID hardop bevestigen, meteen de eerstvolgende taak geven (liefst al vooruit gecheckt terwijl Bob met de huidige bezig is — zie punt hieronder). Klopt het niet: exact melden wat er nog in de export staat, niet aannemen dat "Confirm geklikt" gelijk staat aan "opgeslagen".

  5. Vooruit checken terwijl Bob bezig is: zodra Bob aan een taak begint, kan Claude alvast de eerstvolgende taak in de wachtrij pre-verifiëren (is hij nog steeds nodig, of intussen al door iemand anders/een eerdere sessie opgelost?) — scheelt een aparte heen-en-weer ronde.

  6. Bob's expliciete volgorde-voorkeur (2026-08-11): eerst de volgende taak geven, dán pas de vorige verifiëren — niet andersom. Bob wil na het melden van "gedaan" meteen door kunnen bouwen i.p.v. te wachten tot Claude klaar is met verifiëren; de verificatie van de zojuist afgeronde stap mag daarna, gerust gecombineerd met de volgende voortgangsmelding.

  7. Nooit stilzitten zolang er nog taken liggen (2026-08-14, expliciet bevestigd door Bob). Zodra Bob "gedaan" meldt: altijd meteen de eerstvolgende taak geven, ook voor kleine/optionele restpunten (bv. een laatste losse plek uit een audit) — niet eerst vragen "wil je deze ook nog meepakken, of laten we 'm liggen?". Kies zelf een default (meestal: gewoon meegeven) en ga door; alleen pauzeren met een echte ja/nee-vraag als de keuze de scope wezenlijk verandert (bv. een hele taak overslaan) én Bob niet al liet blijken dat hij liever gewoon doorwerkt.

  8. Bob geeft na afronding van een cluster ook een eigen commit in FlutterFlow's interne versiebeheer ("main"/"Synced" bovenin) — los vangnet naast Claude's export-verificatie.

Altijd het taak-ID noemen bij elke gegeven stap en elke bevestiging (Bob's expliciete voorkeur) — voorkomt verwarring over welk punt van de lijst besproken wordt.

Standaardvraag om een nieuwe sessie mee te starten (plak dit als eerste bericht in een nieuwe chat om meteen door te pakken):

Leg mijn actielijst (TASKS.md) voor me voor, één taak tegelijk — ik voer de builder-stappen zelf uit, jij verifieert daarna met een verse export voor je de volgende geeft. Noem altijd het taak-ID. Begin met [P0 eerst / een specifiek gebied, bv. "Login-pagina" / "verder waar we gebleven waren"].

Variant: Claude bouwt zélf in de builder (bevestigd 2026-08-31)

Naast de pair-fix hierboven werkt ook de omgekeerde verdeling: Claude doet alle builder-klikken via mcp__claude-in-chrome__* in Bob's eigen Chrome, Bob kijkt mee en beslist. Op 2026-08-31 leverde dit in één sessie het complete merk-kleurwerk op (thema-kleuren licht+donker, Roboto, 9 knoppen, 6 tab-indicators, dark mode uit) zonder dat Bob één keer zelf hoefde te klikken.

Voorwaarde die je expliciet moet noemen: Claude gaat hier niet vanzelf van uit — de default is "stappen aanreiken en Bob laten klikken", omdat CLAUDE.md vol staat met waarschuwingen over vastlopende dialogen. Zeg dus letterlijk "je mag in de chrome browser" (of "doe het zelf in de builder") in je eerste of tweede bericht. Dat ene zinnetje was 2026-08-31 het hele verschil.

Blijft gelden: dit werkt alléén terwijl Bob daadwerkelijk achter zijn scherm zit (zie de waarschuwing over de CDP-brug elders in dit bestand), en Claude verifieert nog steeds élke wijziging met een verse export — de builder-UI toont regelmatig een waarde die de export niet haalt.

Standaardvraag voor deze variant:

Pak [taak-ID's] uit TASKS.md op. Je mag zelf in de Chrome-browser in de FlutterFlow-builder werken — ik zit erachter. Verifieer elke wijziging met een verse export voor je verdergaat, en noem het taak-ID. Vraag het aan mij als er een ontwerpbesluit te nemen valt.

Hoeveel taken per sessie — advies: geen harde limiet, maar een sessie werkt het prettigst als hij één samenhangend cluster afmaakt (zelfde FlutterFlow-pagina/component-groep, sluit aan bij de bestaande "groepeer per paginagebied"-vuistregel hierboven) in plaats van kriskras door de hele lijst te gaan — dat scheelt heen-en-weer- navigeren én houdt de context overzichtelijk. Als praktisch richtgetal: 5-10 kleine, mechanische taken (zoals vanavond) is een goede, vermoeidheid-arme klip; grotere/lastigere taken (P1-6's ~45 treffers, P1-15's conditie-gepuzzel) tellen zwaarder en verdienen een eigen sessie of een kleiner deelblok. Sluit elke sessie af met de afsluitroutine hieronder (TASKS.md bijwerken + committen) — dan kan een sessie ook prima na 2 taken stoppen zonder dat er iets verloren gaat.

Sessiegeheugen — afsluitroutine

Aan het eind van elke sessie/taak:

  1. TASKS.md: een afgeronde taak wordt volledig verwijderd (niet gearchiveerd — onnodige context/kosten voor latere sessies). Elke resterende open taak moet zelfstandig te begrijpen zijn (concreet, met bestandspad) zonder de ontstaanschat gelezen te hebben.
  2. CLAUDE.md: alleen aanvullen met blijvend herbruikbare inzichten (conventie, architectuurkeuze, bekend valkuil-patroon) die een latere sessie anders opnieuw zou moeten uitzoeken. Geen sessieverslag/changelog. Ruim verouderde info op i.p.v. eronder te plakken — dit bestand moet klein en scanbaar blijven.
  3. Commit + push de gewijzigde .md-bestanden direct (geen FlutterFlow-export nodig voor pure documentatiewijzigingen).

Geen apart memory-systeem meer nodig voor projectfeiten — die staan allemaal hier en in TASKS.md, wat al elke sessie automatisch geladen wordt. (Auto-memory-bestanden zijn per 2026-08-04 opgeschoond omdat ze dit bestand 1-op-1 dupliceerden.)

FlutterFlow-workflow — belangrijk

Dit project wordt gebouwd via FlutterFlow (app.flutterflow.io). De FlutterFlow-cloudomgeving is de bron van waarheid, niet deze lokale code-export.

  • Bob kan geen code rechtstreeks bewerken in dit repo. Alle wijzigingen aan pagina's, componenten en modellen moeten via de FlutterFlow-website. Enige uitzondering: custom functions/widgets (lib/custom_code/) — ook die voert Bob in via de Custom Code-editor op de website, niet hier lokaal.
  • Gevolg: directe Edit/Write-wijzigingen aan gegenereerde bestanden worden bij de volgende export overschreven — niet duurzaam. Gebruik deze repo om te lezen/ontwerpen/verifiëren (flutter analyze), maar voer het eindresultaat uit via de builder (zelf via browser-automation, of als instructie aan Bob). Ga nooit uit van behoud van een lokale bestandswijziging.
  • Werk je rechtstreeks in de builder (Claude in Chrome): commit na elke afgeronde taak binnen FlutterFlow's eigen versiebeheer ("main"/"Synced" bovenin), met duidelijke omschrijving. Geen lokale git commit.
  • Gebruik mcp__claude-in-chrome__* (Bob's gedeelde Chrome), niet de in-app Browser pane.
  • Resize de browser niet zelf. Bob's eigen gedeelde vensters — meld en vraag i.p.v. zelf te resizen.
  • Bob werkt vaak gelijktijdig zelf in dezelfde builder-sessie — check eerst of hij iets claimde ("dat regel ik zelf") voordat je wijzigingen overschrijft.
  • Check bij een mislukte export/pull eerst het Issues-paneel (badge rechtsboven) voordat je een bug bij jezelf zoekt — een rode teller blokkeert elke export, ook niet-gerelateerde, en is vaak Bob's eigen work-in-progress.
  • De browser-automation naar de FlutterFlow-builder loopt via Bob's eigen, ingelogde Chrome-sessie op zijn machine — dus alleen betrouwbaar terwijl hij actief aanwezig is. Bevestigd 2026-08-09: terwijl Bob een uur weg was, hing een simpele navigate-aanroep naar een FlutterFlow-URL 2x achter elkaar 5 minuten vast (op andere MCP-calls in dezelfde browser, bv. tabs_context, geen probleem) — vermoedelijke oorzaak: een vergrendeld/inactief scherm maakt de extensie/CDP-brug onbetrouwbaar. Praktisch gevolg: builder-UI-werk is geen geschikte taak voor een sessie terwijl Bob niet achter zijn scherm zit (ook niet via /loop of een geplande taak) — Bash/ code-audit/emulator-werk (geen browserafhankelijkheid) juist wel.
  • Valt de extensie middenin een sessie weg ("Claude in Chrome is not connected"), check dan of Chrome überhaupt nog draait vóór je blijft retryen. Bevestigd 2026-09-08: na een uur probleemloos builder-werk gaf elke aanroep opeens die melding. ps aux | grep -iE "google-chrome|chromium" gaf alleen nog crashpad-handlers, terwijl pgrep -a chrome een berg Brave-processen toonde — Bob's Chrome was gewoon afgesloten. Zes retries hielpen dus niets. Een enkele disconnect-melding midden in een browser_batch is wél vaak transient (kwam diezelfde sessie voor en herstelde vanzelf); het verschil zie je aan die ps-check. Draait er geen Chrome: stop met retryen, schrijf de resterende builder-stappen uit in TASKS.md en pak browserloos werk op.

Lokale git-repo + pull/push-workflow

Projectdirectory = eigen schone git-repo, origin ssh://gogs.digitalforce.tv:2222/Uitgaanskrant.com/flutterflow.git, branch master. Los van de grotere/rommelige repo hoger in de mappenstructuur (AndroidStudioProjects, Flutter-SDK e.d.) — commits en pushes horen hier.

Vaste workflow na elke afgeronde taak (staand akkoord, geen aparte bevestiging nodig): niet los flutterflow export-code, maar:

export PATH="/home/bob/fvm/bin:$HOME/.pub-cache/bin:$PATH" && /home/bob/Projects/ff-run-fvm.sh emulator-5554 uitgaanskrant-1qhvtd -s
  • De export PATH=... prefix is verplicht — Bash draait niet-interactief, ~/.bashrc wordt niet geladen. Zonder prefix faalt het script stil ("fvm: command not found" — geen harde error).
  • Standaard testen/QA-doorloop: --profile, niet debug (Bob's besluit 2026-08-21). Aanleiding: na een schone herinstallatie van beide fysieke testtoestellen voelde de app "enorm traag" aan — bleek geen kapot toestel/instelling (Developer Options, animatie-schalen, achtergrondbelasting allemaal normaal, bevestigd via adb shell settings get/top), maar gewoon een debug-build (dumpsys package com.uitgaanskrant.app toonde flags=[ DEBUGGABLE ... ] op beide toestellen) — Flutter's debug-mode is met opzet veel trager (JIT i.p.v. AOT, extra assertions, hot-reload-machinery actief) en geeft dus geen reëel beeld van hoe de app voor een eindgebruiker aanvoelt. Vaste regel vanaf nu: gewoon rondkijken/een fix visueel verifiëren op een device → los van ff-run-fvm.sh direct

    fvm flutter run --profile -d <device_id>
    

    (AOT-gecompileerd, dicht bij release-snelheid, DevTools/profiling blijven bereikbaar, echte runtime-excepties/crashes blijven gewoon zichtbaar — alleen hot-reload/-restart en debug-only asserts vervallen). Volledige debug-mode (ff-run-fvm.sh, met de ingebouwde live-'r'-export-plus-hot-reload-loop) blijft gereserveerd voor het moment dat je daadwerkelijk een specifieke bug actief aan het jagen bent (stack traces nodig, print-debugging, snel itereren op een fix) — niet voor een gewone testronde. ff-run-fvm.sh zelf heeft geen ingebouwde --profile-optie (start altijd kaal fvm flutter run -d <device>, dus debug) — voor profile dus altijd het directe fvm flutter run --profile-commando gebruiken, niet het script.

  • Gradle-build faalt plots met Error resolving plugin [id: 'dev.flutter.flutter-plugin-loader' ...] > <versienummer> + een "restricted method"-warning erboven: check eerst welke Java Flutter gebruikt, niet de code. Bevestigd 2026-08-17: Android Studio update zichzelf via snap automatisch, en de nieuwste snap-revisie bundelt een steeds nieuwere JBR (ingebouwde Java) — Flutter kiest bij het bouwen altijd "de JDK van de nieuwste Android Studio-installatie" als eerste prioriteit. Gradle 8.12 (dit project) kan niet overweg met Java 24+, en het versienummer achteraan die foutmelding is precies de Java-versie van die nieuwe JBR (bv. 25.0.2) — een afgekapte/vervormde foutmelding, geen echte plugin-versie-mismatch. Vaste fix, één keer nodig, overleeft een export (globale Flutter-CLI-instelling, geen projectbestand):

    fvm flutter config --jdk-dir=/usr/lib/jvm/java-17-openjdk-amd64
    

    (systeem-Java 17 is stabiel, geen snap-auto-update-risico). Check bij twijfel welke JDK's beschikbaar zijn met ls /snap/android-studio/*/jbr/bin/java (elke revisie los te aanroepen met -version) en readlink -f /snap/android-studio/current.

  • Een losse exportmap (/tmp/ff-checkN) is niet zomaar te builden: kopieer er eerst .fvmrc, .fvm/ én pubspec.lock uit het project naartoe. Bevestigd 2026-09-03: flutterflow export-code levert geen van drieën op. Zonder pubspec.lock trekt pub get de nieuwste packages binnen; zonder .fvmrc valt fvm flutter terug op de nieuwste geïnstalleerde Flutter (3.44.6) i.p.v. de projectversie (3.35.7). De build faalt dan met misleidende package-fouten diep in ~/.pub-cache (The class 'IconData' can't be extended outside of its library, Couldn't find constructor 'CupertinoPageTransitionsBuilder') — die wijzen naar een SDK-mismatch, niet naar een kapotte dependency. Ga daar dus niet in de packages zoeken; check fvm flutter --version in de exportmap.

  • Interactief script (device-run + hot-restart-loop) → Bash met run_in_background: true. Volg tot minimaal "All done!" én idealiter een succesvolle app-launch (Launching lib/main.dart..., geen nieuwe EXCEPTION CAUGHT BY RENDERING LIBRARY).

  • ⚠️ emulator-5554 is NIET vast de telefoon — de poort hangt af van de opstartvolgorde, niet van het toestel. Bevestigd 2026-09-01: na een herstart van de tablet-service pakte de tablet poort 5554 en de telefoon 5556, precies omgekeerd aan wat hieronder staat. De AVD-namen kloppen ook niet meer met de oude notitie (de tablet heet Medium_Tablet_no-google). Controleer dus altijd eerst wélk toestel je voor je hebt met adb -s <device> shell wm size — tablet = 2560x1600 @ 320dpi, telefoon = 1080x1920 @ 420dpi — vóór je een test of screenshot aan een device-id koppelt.

  • Emulator weg uit adb devices terwijl de systemd-service "active" meldt: kijk naar de luisterpoorten, niet naar de service. Een ss -lnt | grep 555 laat zien of er überhaupt een console-poort geopend is. Twee bekende faalvormen, beide 2026-09-01 gezien: (1) de emulator crasht met code=dumped, status=11/SEGV (zie ook hieronder), (2) bij het herstarten terwijl de andere emulator nog draait komt kvm_user_backed_ram_map: error registering slot: File exists — dan boot hij nooit af. Fix voor (2): beide services stoppen, wachten tot pgrep qemu-system-x86 niets meer geeft, en dan één voor één starten. Onder zware geheugendruk (dit gebeurde bij ~47 GB swap in gebruik) is dit extra waarschijnlijk.

  • mcp__android__screenshot schaalt een tablet-screenshot omlaag — adb shell input tap wil de ECHTE coördinaten. Bevestigd 2026-09-08: de tablet-AVD is 2560x1600, maar de screenshot komt als 1280x800 binnen. Coördinaten die je van zo'n plaatje afleest moet je dus met 2 vermenigvuldigen vóór je ze aan input tap geeft — doe je dat niet, dan land je stelselmatig linksboven van je doel (in dit geval op een kaart i.p.v. op de tabbalk, waarna de app naar een detailpagina navigeerde en de meting waardeloos was). Het Read-antwoord op een screenshotbestand noemt de echte afmeting plus de schaalfactor — lees die even af in plaats van hem aan te nemen.

Testdevices (vanaf 2026-08-09): twee lokale AVD's, geen fysieke toestellen meer. Eén telefoon-AVD (licht_avd, laag RAM-profiel) en één tablet-AVD (Medium_Tablet_no-google) — welke van de twee op poort 5554 en welke op 5556 zit wisselt met de opstartvolgorde, dus ga daar nooit vanuit (zie de waarschuwing hierboven; op 2026-09-03 was 5554 de tablet). Beide starten automatisch bij Bob's desktop-login via systemd user-services (~/.config/systemd/user/android-emulator*.service). WayDroid en losse fysieke devices zijn niet meer in gebruik (WayDroid's adb shell screencap leed structureel aan PTY/CRLF-corruptie bij screenshots — vandaar de overstap). Check altijd eerst met adb devices welke van de twee daadwerkelijk draaien vóórdat je ff-run-fvm.sh of de android-mcp-tools gebruikt — draait geen van beide: navragen bij Bob i.p.v. blind op het eerste beschikbare device te draaien. De android-mcp-server-tools (mcp__android__*) accepteren een device_id-parameter per call, dus beide devices zijn los aan te sturen zonder config-wijziging — handig om ontwerp/layout op zowel telefoon- als tabletformaat te verifiëren (bv. relevant voor P1-11's responsive-grid-taak). Correctie 2026-08-17: "geen fysieke toestellen meer" is niet meer hard waar — Bob gebruikt voor een live pair-fix soms ook zijn eigen fysieke Xiaomi/MIUI-telefoon (adb-serial bv. K7V8DYTSMVTW6XBI, ro.product.model=2409BRN2CY), vooral geschikt om een device met echte on-screen 3-knops-navigatiebalk te testen (de twee AVD's hebben dat niet). Check dus met adb devices wat er nu verbonden is i.p.v. aan te nemen dat het altijd een van de twee AVD's is. Let op MIUI- specifieke ANR's bij snelle geautomatiseerde adb-input: op dit fysieke toestel triggerde een korte reeks losse adb shell input text/input tap-calls achter elkaar (zonder pauze) herhaaldelijk een "Uitgaanskrant.com isn't responding"-dialoog (MIUI's eigen "MIUIScout"-mechanisme, zichtbaar in logcat als W MIUIScout ANR) — dit is een MIUI/debug-build-artefact van de invoersnelheid, geen crash van de app zelf. Bouw bij dit toestel bewust een korte sleep/wachttijd tussen opeenvolgende input-acties in i.p.v. ze direct na elkaar te vuren.

  • Screenshots van de website zelf (voor look&feel-vergelijkingen): headless Chromium werkt, maar met twee valkuilen. (1) Snap-Chromium mag niet schrijven in /tmp/claude-* (Permission denied) — schrijf naar ~/uk-shots/ o.i.d. (2) De homepage opent met een cookiebanner + een JS-modal die het hele scherm vult, en --blink-settings=scriptEnabled=false laat --screenshot stil falen. Werkend recept: pagina ophalen met Python, <script>-tags strippen, <base href="https://uitgaanskrant.com/"> in de <head> zetten, lokaal opslaan en dán chromium --headless=new --window-size=430,1500 --screenshot=x.png file://... — CSS en afbeeldingen laden gewoon via de base-href.
  • Telefoonformaat-screenshots zonder de telefoon-emulator te claimen: de tablet-AVD is tijdelijk om te zetten met adb -s <tablet> shell wm size 1080x2160 + wm density 420, en daarna terug met wm size reset / wm density reset. Handig wanneer op de telefoon-AVD al een sessie van iemand anders draait die je niet wilt verstoren. Werkt in de praktijk goed (2026-09-03: zo zijn de 1/2/3-kolomcontroles van P2-21 op één AVD gedaan — 1280 dp liggend, 800 dp staand via wm size 1600x2560, en 411 dp op telefoonformaat). Vergeet de reset niet, anders vindt de volgende sessie een tablet die zich als telefoon voordoet.
  • AVD crasht/valt soms zelf uit (los van de app) — diagnose via journalctl --user -u android-emulator.service (telefoon) of ...-tablet.service (tablet), niet via adb logcat. Bevestigd 2026-08-21: emulator-5554 crashte zelfstandig tijdens een sessie (systemctl --user status toonde code=dumped, signal=SEGV, met een geheugenpiek naar 20GB vlak ervoor) — de service zelf (en dus journalctl) overleeft zo'n crash en laat de stacktrace/reden zien; het device verdwijnt gewoon uit adb devices zonder verdere uitleg. systemctl --user show <service> -p NRestarts laat zien of systemd 'm al automatisch herstart heeft (Restart=on-failure staat aan, dus meestal komt-ie vanzelf terug op een active running na 10-30s). Valkuil bij het meten van "gebeurt dit nu live?" via adb logcat: zonder eerst adb logcat -c (buffer legen) dumpt een nieuwe adb logcat-aanroep eerst de hele bestaande ring buffer in een korte burst (kan makkelijk 20.000+ regels/paar seconden zijn, inclusief oude boot-logs) vóórdat 'm daadwerkelijke live regels toont — een timeout 3s adb logcat | wc -l op een ongeleegde buffer geeft dus een compleet misleidend "er gebeurt hier van alles"-beeld. Altijd adb logcat -c vóór een korte live-meting.
  • Een EXCEPTION CAUGHT BY RENDERING LIBRARY/crash tijdens de run is niet per se een regressie van je eigen wijziging — check eerst de stack trace op bestandsnaam. lib/kanweg/ is Bob's eigen scratch-testgebied (zie Opschonen hieronder) en kan legitiem crashen zonder dat dat iets met de huidige taak te maken heeft; de app kan daar staan door een eerdere hot-restart die Bob's laatst bezochte route onthield, niet per se doordat het de echte initialLocation is (check nav.dart).
  • Slechts één volledige EXCEPTION CAUGHT BY RENDERING LIBRARY-dump per proceslevensduur, ongeacht hoeveel verschillende widgets daarna overflowen. Flutter's dumpErrorToConsole toont de volledige stack trace + widget/bestand/regel alleen bij de allereerste exceptie sinds process- of hot-restart; elke volgende (ook een heel andere widget/pagina) wordt ingekort tot Another exception was thrown: <bericht> zonder bestandslocatie. Bevestigd 2026-08-09 (P1-13-onderzoek): de Home-pagina's al bekende overflow (PUitgaanSliderKaartComponent, P1-3) consumeerde de enige volledige dump nog vóórdat er genavigeerd kon worden naar de eigenlijk onderzochte pagina — alle latere overflows op die andere pagina bleven anoniem. Om een verse stack trace voor een specifieke, latere crash te pakken: eerst een hot-restart (R) sturen vlak vóór je die crash reproduceert, ván een schone/crash-vrije pagina uit — dat reset de teller. Een run_in_background-Bash-aanroep van ff-run-fvm.sh heeft stdin op /dev/null (niet-interactief), dus r/R is dan niet te versturen. Kale mkfifo alléén werkt niet (Flutter's keyboard-handler test op een echte TTY vóórdat hij raw keypresses verwerkt — een FIFO is er geen, dus de R komt nooit aan, ook al accepteert flutter run de pipe zonder foutmelding). Bevestigd werkend recept (2026-08-09): setsid op zowel de FIFO-writer als de flutter run-aanroep zelf, elk in een eigen run_in_background-Bash-call:

    mkfifo /pad/naar/fifo
    setsid bash -c "exec 9>'/pad/naar/fifo'; sleep 3600" < /dev/null > /dev/null 2>&1 &
    disown
    # aparte tool-call:
    setsid script -qc "fvm flutter run -d <device>" /pad/naar/logfile < /pad/naar/fifo > /dev/null 2>&1 &
    disown
    # later, weer een eigen call, om te restarten:
    printf 'R' > /pad/naar/fifo
    

    setsid geeft beide achtergrondprocessen een eigen sessie los van de Bash-call die ze startte, dus ze overleven gewoon over meerdere losse tool-calls heen. Flutter DevTools (de devtools-URL uit dezelfde run-log) is zelf ook een Flutter-Web-canvas-app zonder toegankelijke DOM — geen bruikbare omweg om alsnog bij de volledige historische foutenlijst te komen. Update 2026-08-09: het R+deep-link-race-probleem is opgelost — gebruik --route bij het starten in plaats van een hot-restart + losse deep-link. fvm flutter run -d <device> --route "/pad?param=waarde" zet Flutter's defaultRouteName al vóór de eerste frame, dus go_router opent direct die pagina — een tussenliggende Home-build (en dus Home's eigen overflow, die anders de enige volledige-dump- slot opsoupeert) vindt niet plaats. Geen FIFO/R/am start meer nodig wanneer de doelpagina zelf met een directe route+query-param bereikbaar is. Een hot-restart (R) blijft altijd terugvallen op de initialLocation/Home, en een deep-link-am start vlak daarna wordt door de net-herstarte app niet opnieuw verwerkt (adb toont dan "Activity not started, intent has been delivered to currently running top-most instance" terwijl de app toch op Home blijft) — dus die combinatie blijft de dump-slot-race verliezen. Het FIFO/R- recept hierboven blijft wel nodig voor pagina's die niet met een kale --route te bereiken zijn (bv. een crash die pas na meerdere gebruikersacties optreedt).

Daarna automatisch (of gebruik ff-commit.sh, zie hieronder):

  1. git status --short, dan git addniet blind -A. Alleen echte FlutterFlow/app-wijzigingen (lib/, android/, ios/, pubspec*, .gitignore). .claude/ blijft uitgesloten (sessiestate, geen app-code). Bob's eigen concurrente wijzigingen horen gewoon mee in dezelfde commit.
  2. git commit met duidelijke boodschap.
  3. git push (-u origin master als tracking nog niet staat).

Valkuil: flutterflow export-code overschrijft .gitignore bij elke export terug naar FlutterFlow's standaardversie. Voeg na elke export, vóór staging, deze regel weer toe als hij ontbreekt:

# Claude Code session state (not app code)
.claude/

(CLAUDE.md zelf overleeft een export altijd — check voor de zekerheid toch even.)

/home/bob/Projects/ff-commit.sh (2026-08-09) automatiseert bovenstaande 3 stappen + de .gitignore-valkuil-check in één keer:

/home/bob/Projects/ff-commit.sh "commit message"          # app-code modus (lib/android/ios/pubspec*/.gitignore)
/home/bob/Projects/ff-commit.sh --docs "commit message"   # alleen CLAUDE.md/TASKS.md

Herstelt automatisch de .claude/-gitignore-regel, staget alleen wat echt gewijzigd is binnen de relevante paden, toont wat wél/niet meegaat, en vraagt bevestiging vóór commit+push. Optioneel --dir <pad> voor een ander projectpad dan de default (ff-app/uitgaanskrant-1qhvtd).

Eerst research, dan bouwen

Vóór een niet-triviale taak (bugfix, nieuw patroon, integratie): kort online zoeken naar bestaande oplossingen i.p.v. zelf trial-and-error in de builder. Geldt niet voor mechanische herhaling van een patroon dat al bevestigd werkt.

FlutterFlow-builder: bekende problemen & patronen

Een Custom Function met een lokale geneste closure/functie in de body (final x = (String input) { ... }; of String x(String input) { ... } binnen de functie zelf) laat "Save Function" mislukken met "The function is empty or cannot be parsed" — ook als de Dart-code zelf 100% valide is. Bevestigd 2026-08-25 (P2-6, filterHorecagelegenheden): een custom function die intern een kleine helper-closure declareerde (voor tekst-normalisatie, hergebruikt op 2 plekken in de body) kreeg deze foutmelding bij elke opslagpoging, ondanks een correct geformatteerde, brace-gebalanceerde body. Pas na het volledig verwijderen van de geneste closure (logica in plaats daarvan inline uitgeschreven, 2x gedupliceerd i.p.v. 1x als helper) sloeg de functie wél op. FlutterFlow's eigen validator lijkt dus een lichte, niet-volledige parser te gebruiken die struikelt over een 2e (params) { ... }-patroon binnen de functie-body. Vuistregel: custom-function-bodies plat/zonder lokale closures schrijven; heb je gedeelde logica nodig, maak er een aparte top-level Custom Function van (die roep je dan gewoon aan vanuit de andere) i.p.v. een lokale helper te nesten.

getJsonField() is NIET beschikbaar binnen een Custom Function — wel binnen een Custom Action. Bevestigd 2026-08-31 (filterHorecagelegenheden): die helper komt uit /flutter_flow/flutter_flow_util.dart, en dat wordt bij Custom Functions niet mee-geïmporteerd; het import-blok bovenaan een Custom Function is bovendien niet te bewerken. Gevolg: dart analyze geeft The function 'getJsonField' isn't defined. Fix: gewone Map-toegang gebruiken ((item is Map ? item : null)?[key]), en als je JSON-Path-achtige argumenten binnenkrijgt eerst de $.-prefix strippen (pad.startsWith(r'$.') ? pad.substring(2) : pad). Werkt voor paden van 1 niveau diep, wat in de praktijk vrijwel altijd het geval is. Custom Actions krijgen flutter_flow_util.dart wél automatisch, dus daar mag getJsonField() gewoon.

Analysefouten van een verse export zijn vals zolang je geen flutter pub get in die exportmap hebt gedraaid. Een kale flutterflow export-code-map heeft geen .dart_tool/package_config.json, waardoor dart analyze élke package-import als Target of URI doesn't exist: 'package:http/http.dart' markeert plus allerlei vervolgfouten (getJsonField isn't defined, ProvincieModelStruct isn't a type, …). Dat leidt gegarandeerd tot verkeerde conclusies over "kapotte" custom code. Vaste volgorde bij het verifiëren van een export:

cd /tmp/ff-checkN && fvm use 3.35.7 -f && fvm flutter pub get && fvm dart analyze lib/

en tel dan alleen de regels die met error - beginnen (een gezond project geeft hier ~2000 info/warning-regels en 0 errors — laat je niet afschrikken door dat totaalgetal).

De fvm use 3.35.7 -f erbij is geen overbodige luxe (2026-09-03). Een verse exportmap heeft géén fvm-pin, dus fvm flutter valt daar terug op de globale/snap-Flutter (nu 3.44.6). Wil je in zo'n map ook échte builden (fvm flutter run), dan faalt dat met misleidende fouten in packages die je niet zelf schreef: The class 'IconData' can't be extended outside of its library because it's a final class (font_awesome_flutter) en Couldn't find constructor 'CupertinoPageTransitionsBuilder' (page_transition). Dat lijkt op kapotte dependencies maar is puur de verkeerde SDK-versie. Eén fvm use 3.35.7 -f in die map lost het op.

API Call-URL: relatief pad hoort bij een API Group, absolute URL bij een top-level call. Bevestigd 2026-08-31 (PlaatsenBijGemeente): een top-level call aangemaakt mét een relatief pad (/nl/flutterdrup/plaatsen_bij_gemeente.json) genereert precies dat als apiUrl — zónder host, dus de call faalt. Binnen een groep hoort juist het relatieve pad, want daar plakt FlutterFlow ${XGroup.getBaseUrl()} ervoor. Herkenbaar in de export: een group-call is een instance-methode (Future<ApiCallResponse> call() op een XGroup-klasse, een top-level call is static (static Future<ApiCallResponse> call(). (De groep in dit project heet sinds 2026-08-31 productie i.p.v. kanwegProductieGroup, base-URL https://uitgaanskrant.com.)

De Custom Code-editor (Function/Action): tekst selecteren via toetsenbord (Ctrl+A, Home+Shift+Ctrl+End) en daarna Delete/Backspace drukken werkt structureel niet — de selectie is zichtbaar (blauw gemarkeerd) maar Delete/Backspace laat de tekst ongewijzigd staan. Bevestigd 2026-08-25: herhaaldelijk getest (los Ctrl+A, los Home+Shift+Ctrl+End, beide gevolgd door zowel Delete als Backspace) — nooit een zichtbare wijziging, ondanks een overduidelijk gemarkeerde selectie. Losse toetsen als Ctrl+Z (undo) en gewoon typen werken wel gewoon. Werkende omweg: typen ZONDER voorafgaande delete-actie overschrijft de selectie namelijk ook niet (bevestigd, het getypte kwam ernaast/erin terecht i.p.v. de selectie te vervangen) — dus bij een foute/verouderde inhoud is er geen betrouwbare manier om gericht te muteren; de enige robuuste route is Cancel → "Discard new custom code?" → Yes en volledig opnieuw beginnen met één ononderbroken type-actie in een leeg codeveld. Een geslaagde eerste-keer-typing in een vers/leeg codeveld werkt wél betrouwbaar; het is specifiek achteraf-bewerken dat corrumpeert (waargenomen: oude en nieuwe tekst raken door elkaar gemengd, bv. Strinal uit een samengevoegde String/final).

Een Project Component naar een specifieke plek in de Widget Tree slepen (canvas-dropzone, zie het bestaande recept elders in dit bestand) kan structureel mislukken als een fixed-size kind-widget (bv. een 200×200px Image) de rest van zijn ouder-Column visueel overlapt/overflowt. Bevestigd 2026-08-25 (P2-14, horecagelegenheid_current_widget.dart): de Info-tab-Column (Image + Text-adres/Text-plaats/... eronder) rendert in design-time zo dat de vaste 200×200px logo-Image de hele Column overdekt, ook het stuk waar de tekstregels eronder eigenlijk staan — een sleep-drop op die coördinaten target daardoor altijd de Image zelf ("Image does not accept children"-toast) in plaats van de Column. Erger: soms landt de drop zelfs op een totaal andere, elders-in-de-tree-liggende component die toevallig dezelfde canvas-coördinaten inneemt (design-time-overlap, geen echte nesting-relatie) — bevestigd via een expliciete tree-selectie die een heel ander component-type toonde dan verwacht op exact dezelfde pixel. 10+ pogingen met verschillende zoomniveaus (90%/150%) en doelcoördinaten gaven telkens hetzelfde resultaat — dit is dus geen "net iets anders mikken"-probleem maar een structurele canvas-hit-testing-beperking bij overlappende/overflowende widgets. Geen werkende omweg gevonden (Insert-Widget-dialoog op de Column zelf toont geen Project Components, alleen kale basis-widgets). Bij dit patroon: niet blijven proberen, teruggeven aan Bob met de exacte Widget-Tree-locatie (component + parameter- bindings) — in zijn eigen browser ziet hij gewoon waar hij klikt, geen coördinaat-giswerk nodig.

**Een pagina kan in een staat komen waarin de builder élke wijziging weigert met een generieke "Invalid Action: The most recent action would have caused a crashing error, so we've undone it for you"-toast

  • automatische terugdraai — ook voor acties die met elkaar niets te maken hebben.** Bevestigd 2026-08-25 (Bob, P2-7, pagina Event onder de "Evenement"-map, bevestigd orphan/nergens naartoe genavigeerd): hernoemen (naar een kanweg_-prefix), verplaatsen naar de kanweg-map, én een los component (HeaderButtonsComponent/ Drawer) verwijderen gaven alle drie dezelfde blokkade — ook na een harde browser-reload. grep op de geëxporteerde code bevestigde 0 externe referenties naar deze pagina; de blokkade zit dus puur in FlutterFlow's eigen interne project-graaf, niet in iets dat via de export zichtbaar of van hieruit op te lossen is. Geen bekende fix gevonden — als dit optreedt: 1-2 pogingen + 1 reload, dan accepteren en de pagina met rust laten (geen functioneel risico als de pagina toch al onbereikbaar is), niet verder tijd insteken.

Een letterlijke tekst met 1 ingesloten variabele (bv. een JSON-body {"entity_id": <nid>, "entity_type": "node"}) als waarde van een Custom-Action-String-argument: geen concatenatie-optie in het argumentenpaneel — bouw een kleine Custom Function. Bevestigd 2026-08-24/25 (P1-7 Restpunt A, horeca-hartjes Drupal-sync): het "Value"-veld van een Custom Action-argument (bv. drupalRequest's body) accepteert óf vrije typetekst (puur literal) óf een volledige vervanging door 1 variabele via het icoontje naast "Value" (Set Variable-dialoog) — nooit allebei tegelijk. De dialoog's "Source"-lijst toont wel een optie "Combine Text", maar niet verder onderzocht (bleek niet nodig zodra de Custom-Function-route werkte). Werkende oplossing: een nieuwe Custom Function toevoegen (Custom Code-tab → + → Function), bv. String favorietenBodyNode(String nid) => '{"entity_id": ' + nid + ', "entity_type": "node"}'; — daarna is die functie gewoon kiesbaar als waardebron in het Value-icoontje-dialoog (Source → Custom Functions), met een eigen geneste Set-Variable-stap per functie-argument om dat argument aan de juiste component-/page- parameter te binden. Zelfde procedure ook bruikbaar voor andere "body/URL met 1 variabele"-gevallen. Bijvangst: de geneste Set-Variable-dialoog (voor het functie-argument) liep hier 2x tegen de al bekende freeze aan (zie hieronder) — sleep 'm bij twijfel meteen omhoog (drag op de titelbalk) vóórdat je gaat zoeken, dat voorkwam de freeze in de 3e/4e poging elke keer.

Een pagina die nog geen Drawer/AppBar heeft alsnog toevoegen (bv. een pagina zonder header/hamburger-menu): via slepen, niet via rechtsklik. Gevonden 2026-08-19 (Favorieten-pagina miste een header/drawer volledig). Stappen:

  1. Selecteer de root Scaffold-node in de Widget Tree → klik het kleine "+"-icoontje naast de node-naam (niet rechtsklik → Insert Widget, dat menu heeft geen Drawer/AppBar-optie op dit niveau) → zoek "Drawer" (of "App Bar") → kies de kaart. Dit voegt een lege Drawer/AppBar-slot toe als eigen tree-node, sibling van body.
  2. Om die slot te vullen met een custom Project Component (bv. drawerComponent, HeaderButtonsComponent): rechtsklik/Insert Widget op de nieuwe Drawer/AppBar-node werkt niet (geen "Insert Widget"-optie in het contextmenu, en de node zelf toont geen "+"-icoontje in de tree). Werkende route: open het linker widget-paneel (Build-tab, 1e icoon) → 2e icoon in dat paneel se­ lecteert "Project Components" → zoek de component op naam → sleep de kaart direct naar de juiste canvas-dropzone (bij een AppBar toont de canvas tijdens het slepen zelf de "Leading"/ "Title"/"Actions"-zones; bij een Drawer volstaat een sleep ergens op het lege-Drawer-canvas). Voor een kale generieke widget (bv. "Text") werkt exact dezelfde sleep-techniek vanuit het gewone Elements-paneel (1e icoon in dat linkerpaneel).
  3. Een component met een verplichte parameter (bv. HeaderButtonsComponent's showBackButton) krijgt na het slepen gewoon een normaal Component Parameters-blok in het rechterpaneel — zet die zoals altijd (zie ook P1-20's showBackButton-precedent verderop in dit bestand: false voor een drawer-nav-bestemming zoals Home, niet voor een subpagina).
  4. Let op dubbele hamburger-knop: als de nieuwe AppBar een HeaderButtonsComponent als title krijgt (die zelf al een eigen Scaffold.of(context).openDrawer()-knop bevat) én er inmiddels ook een Drawer op de Scaffold staat, zet FlutterFlow automatisch automaticallyImplyLeading: true — dat genereert een 2e, overbodige hamburger-knop naast de component's eigen knop. Fix: AppBar-node selecteren → Visibility-sectie → "Show Default Button" uit (genereert automaticallyImplyLeading: false, exact het patroon dat de bestaande pagina's al gebruiken).
  5. Tab-labels die afkappen op een TabBar met veel/lange tabs: geen losse widget-wrap nodig — de TabBar-node zelf heeft een "Tab Bar Scrollable"-toggle (Search properties → "scroll"). Zet aan: labels tonen voortaan volledig uitgeschreven en de balk scrollt horizontaal i.p.v. tekst af te kappen.

Widget Tree-nesting nooit uit een codefragment afleiden — altijd de tree/screenshot als bron van waarheid gebruiken. Bevestigde misser (2026-08-11, HorecagelegenheidoverzichtKaartWidget): op basis van een gedeeltelijke Read (alleen regels 280-343) leek de hartje- AlignedTooltip genest in de Stack (samen met de afbeelding/ categorie-tags), puur op basis van de zichtbare inspringing in dat fragment. Bob's eigen widget-tree-screenshot liet zien dat Tooltip in werkelijkheid een directe sibling van Stack is (beide rechtstreeks kind van Row), niet genest erin — de inspringing in een afgekapt codefragment is geen betrouwbare graadmeter voor nesting zonder de volledige haakjes-balans te checken. Vuistregel: geef builder-pad-instructies pas na het zien van een tree-screenshot (of een volledige, haakjes-sluitende Read van het component), niet op basis van een korte code-grep/fragment-inferentie — bij twijfel eerst om een screenshot vragen i.p.v. te gokken.

⚠️ De Visibility → Conditional-toggle UITZETTEN wist de conditie — weer aanzetten geeft een lege conditie (Unset in rood), geen herstel. Bevestigd 2026-09-03 (DropDownGemeenten-mijngemeenten op stadsactiviteitAanmaken, tijdens P2-22-onderzoek). De toggle is dus geen aan/uit-schakelaar waarmee je even iets kunt uitproberen: uitzetten is destructief voor de conditie zelf. Noteer de conditie vóór je 'm uitzet (bron + JSON Path + operator), want je moet 'm daarna volledig opnieuw opbouwen via de Set-from-Variable-dialoog. Een aangezette conditional zónder conditie genereert bovendien géén guard in de export — de widget is dan gewoon altijd zichtbaar, zonder dat de builder daar duidelijk over is.

Conditioneel tonen/verbergen: gebruik de ingebouwde "Visibility"-sectie op het widget's eigen properties-paneel, niet Wrap Widget. Elk widget heeft rechtsboven in zijn eigen properties-paneel een "Visibility"- sectie (Conditional-toggle, Responsive per-device zichtbaarheid, Opacity-slider) — toggle "Conditional" aan om een conditie te zetten, geen wrap nodig. Bevestigd 2026-08-13. Correctie: de "Wrap Widget"-grid zelf bevat inderdaad géén "Visibility"-optie (volledige grid: Container, Card, Column, Row, Stack, ListView, GridView, Wrap, Form Validation, Blur, MouseRegion, Transform, Tooltip, ConditionalBuilder, Draggable, DragTarget, Flex, ShaderWrapper, AspectRatio) — dat klopt nog steeds, maar is de verkeerde plek om te zoeken. Wrap Widget → ConditionalBuilder blijft wél nodig wanneer je een widget conditioneel wilt vervangen door iets anders (een echte If/Then/Else met verschillende content per tak) — de losse Visibility-sectie hierboven is puur aan/uit, geen alternatieve inhoud.

**Een widget Expanded/Flexible maken: "Expanded" staat niet in de Wrap Widget-grid (zie de volledige lijst hierboven) — gebruik Wrap Widget → Flex, en zet daarna op die nieuwe Flex-node de sectie "Expansion" (bovenaan zijn properties-paneel, vóór "Padding" — 3 icoontjes, geen tekstveld/zoekbaar via "fit"). Bevestigd 2026-08-23 (Favorieten Tab 2, ListView zonder begrensde hoogte gaf een RenderFlex-overflow): het laatste van de 3 Expansion-icoontjes bleek Expanded te zijn, gaf na Confirm Expanded(child: Flex(direction: Axis.vertical, ...)) — exact het gewenste effect (overflow weg). Volgorde van de 3 icoontjes (None/Expanded/Flexible) niet met zekerheid getest voor andere widget-types; bij twijfel gewoon proberen en met een verse export verifiëren welk Dart-attribuut (Expanded(...) vs Flexible(...)) het echt genereerde — kost niets, geen destructieve actie.

ConditionalBuilder instellen na een Wrap Widget: klik op de ConditionalBuilder-rij zelf, niet op If. Na "Wrap Widget → ConditionalBuilder" selecteert de builder automatisch de If-tak (teal gemarkeerd) zodat je meteen widgets in de THEN-kant kan zetten — maar dat rechterpaneel toont dan de If-tak-eigenschappen, niet de conditie zelf. Klik op de ConditionalBuilder-rij (één niveau hoger in de tree) om de sectie "Conditions" → "Add Condition" → "Single Condition" (First Value/Operator/Second Value) te krijgen.

"Set from Variable"-dialoog (Visibility → Conditional, of First/ Second Value binnen een Single Condition): de "Conditions" → "Single Condition"-suboptie rendert vaak leeg/onklikbaar bij de eerste expand-klik. Bevestigd structureel (2026-08-18, meerdere velden op evenement_horecagelegenheid_widget.dart/horecagelegenheid_current_widget.dart): na klikken op "Conditions" blijft de ruimte voor "Single Condition"/ "Combine Conditions" leeg (wel gereserveerde hoogte, geen zichtbare/ klikbare tekst) totdat je 2-4x extra op "Conditions" klikt — een zoom-screenshot bevestigt dat de tekst er wél staat, alleen niet in de normale (gecomprimeerde) screenshot-resolutie. Betrouwbaardere route: typ in de zoekbalk bovenaan de dialoog (bv. "Single") vóórdat je op "Conditions" klikt — de gefilterde lijst toont dan alleen "Conditions" met exact 1 suboptie eronder, waardoor de klikpositie niet meer geraden hoeft te worden. Zelfde aanpak werkt voor het latere "UNSET"-veld binnen First/Second Value (klik toont soms alleen een "+"/"-"-toggle zonder de eigenlijke UNSET-rij; gewoon nogmaals klikken op dezelfde positie totdat de rij verschijnt, meestal 2-3x).

Checken of een List<String> App State-variabele een waarde bevat ("bevat dit item?"): geen aparte condition-operator, maar een transform binnen "Set from Variable". Bevestigd 2026-08-11 op favorieteHorecaNids (First Value van een Single Condition, Type: Boolean): klik het Value-icoontje → Set from Variable → kies de App State-lijst-variabele als bron → het "Available Options"-dropdownmenu (zelfde UI-plek als de al bekende JSON-Path-optie, zie hieronder) biedt dan lijst-transforms: List Contains Item, Filter List Items, Item at Index, First Few Items, Is Set and Not Empty, Sort List Items, Unique List Items, Number of Items. Kies "List Contains Item" → vul het item-veld dat verschijnt met de te zoeken waarde (bv. een Component Parameter zoals nid) → dit levert een Boolean terug. Terug in de Single Condition: Operator "Equal to", Second Value = literal true. Dit patroon komt vaker terug (favoriete-lijst-checks op meerdere plekken) — niet opnieuw naar een "Contains"-operator in de conditie zelf zoeken, die bestaat niet; ga altijd via deze Set-from-Variable-transform-route op de lijst-variabele.

Thema-kleuren en -lettertype zetten (Theme Settings) — werkend recept 2026-08-31, Claude, volledig via browser-automation gelukt. Theme Settings zit niet op ?tab=theme (die URL redirect stil naar de laatst geopende pagina) maar op ?tab=themeSettings&themeTab=color; in de linkerrail is het het palet-icoontje net bóven het tandwiel onderaan. Een kleur wijzigen: klik het swatch-blokje → "Choose a Color"-dialoog → triple-click het hex-veld → typ de 6 tekens zonder # → Return → sluiten met de X rechtsboven. Lettertype: knoppen "Primary Font Family"/"Secondary Font Family" rechtsboven op de Typography-tab; Roboto staat bovenaan de Google-Fonts-lijst.

  • Kritieke timing-valkuil: de dialoog opent traag en vaak pas bij de twééde klik. Een browser_batch van "klik swatch → typ → Return" faalt daardoor stil (de klik landt, de dialoog verschijnt pas ná de typ-actie, en het paneel toont daarna gewoon de oude waarde — geen foutmelding). Vaste werkwijze: één call om de dialoog te openen + te verifiëren (screenshot/zoom), een tweede call om te wijzigen en te sluiten. Reken op 6-8 seconden wachttijd na de openings-klik, en klik desnoods een tweede keer als de eerste niets deed.

Drie kleuren die op tientallen plekken in de export staan komen NIET uit de widgets zelf — zoek eerst de centrale instelling voordat je gaat klikken. Bevestigd 2026-08-31 (P1-33, 93 hardcoded hexen → 12, en die 12 zitten alleen nog in dode kopieën):

  • Laadspinner (SpinKitFadingCircle, in dit project 42× identiek, inclusief één in nav.dart die FlutterFlow zelf genereert): Theme Settings → Design System → "Loading Indicator" — één blok met Indicator Type / Color / Size (Diameter) + live preview. Eén wijziging daar raakt élke FutureBuilder-laadstaat in het hele project. Staat niet in App Settings → App Details → UI Settings (die sectie bevat alleen "Show Component Preview in Palette") en niet onder Theme Widgets (dat is voor eigen widget-stijlen).
  • AppBariconTheme is de "Default Button Color" van de standaard-terugknop, per pagina. Die eigenschap is alleen zichtbaar als "Show Default Button" aan staat — op vrijwel elke pagina hier staat hij uit (automaticallyImplyLeading: false), waardoor de hex in de export staat maar niets verft. Wél op te ruimen: toggle aan → kleur op een thema-token → toggle weer uit; de binding blijft staan (geverifieerd via export). De projectbrede App Bar-stijl staat los daarvan onder App Settings → Nav Bar & App Bar.
  • Losse FlutterFlowIconButton-kleuren zijn wél per widget: let op dat fillColor (de knopvorm) en Icon Color (het icoon erin) twee verschillende velden zijn — een hartje-op-wit-vlak zit in Icon Color, een hamburger-op-gekleurd-vlak in Fill Color.

Duplicate Page kopieert Upload-Data-actienamen mee, en die moeten projectbreed UNIEK zijn — een duplicaat blokkeert daardoor meteen de export. Bevestigd 2026-09-02 (stadsactiviteitAanmaken gedupliceerd naar uitgaansevenementAanmaken): het Issues-paneel meldt Name for Upload Data action on <knop> is not unique, en flutterflow export-code faalt met de generieke Status: 400 / Error generating code for the project. Fix: knop → Actions-tab → de "Store media for upload"-actie → onderaan het rechterpaneel het veld Name een unieke waarde geven. Het Name-veld valt standaard buiten beeld: klik het "|<"-icoontje linksboven in de Action Flow Editor om de trigger-kolom in te klappen — dan schuift het rechterpaneel mee en staat het veld gewoon in beeld. (Dat icoontje is ook los nuttig tegen de bekende paneel-clipping.) Reken er bij elke gedupliceerde pagina met uploads op dat je dit per upload-knop moet doen.

Een Custom Action met veel argumenten: de ONDERSTE rij van "Set Action Arguments" is voor Claude onbereikbaar — die moet Bob zetten. Bevestigd 2026-09-03 (evenementCreate, 16 argumenten, op uitgaansevenementAanmaken): het argumentenpaneel in de Action Flow Editor scrollt niet — niet met het muiswiel, niet door de scrollbar-thumb te slepen, en niet met Page Down na een klik in het paneel. Elk argument één voor één inklappen via zijn chevron helpt wel (dat werkt betrouwbaar, ±2 per tool-aanroep), maar bij 16 argumenten valt de Value-rij van het láátste argument daarna nog steeds ±10px onder de onderrand van de overlay en rendert hij domweg niet. Ook het aanklikken van de fout in het Issues-paneel scrollt er niet naartoe. Vuistregel: bind de argumenten van onder naar boven zolang dat kan, en reken erop dat het laatste argument van een lange lijst aan Bob overgedragen moet worden (in zijn eigen, hogere venster staat het gewoon in beeld). Noem daarbij letterlijk de doelwaarde en géén alternatieven in dezelfde zin — een terzijde in een keuzevraag ("X… nee, Y") leverde 2026-09-03 prompt de verkeerde binding op.

Een dropdown die zichzelf moet voorselecteren: de waarde moet op TWEE plekken gezet worden, want de lijst leeft alleen binnen de FutureBuilder. Bevestigd 2026-09-03 (DropDownHorecagelegenheid op uitgaansevenementAanmaken). De data waar je op wilt voorselecteren zit in de Backend Query van de dropdown zelf; een widget daarbuiten (bv. de verzendknop met een Visibility-conditie op page state) ziet die nooit. Bovendien rebuildt bij het klaarkomen van die future alleen de FutureBuilder-subtree — een sibling-widget hoger in de Column evalueert zijn conditie niet opnieuw, dus "bind de knop gewoon aan Widget State van de dropdown" werkt in de praktijk niet. Werkend patroon:

  1. één custom function die de voorselectie bepaalt (bv. page parameter als die gezet is, anders het enige item uit de lijst, anders null);
  2. Dropdown → Initial Option Value = die functie over de eigen backend response (API Response Options: JSON Body, Available Options: No Further Changes — niet JSON Path, je wilt de hele array). Dit genereert _model.dropDownXValue ??= <functie> en vult dus ook de widget state;
  3. On Page Load = een losse Backend Call naar hetzelfde endpoint + Update Page State met dezelfde functie over die action-output. Die keten eindigt op safeSetState, waardoor de knop wél opnieuw evalueert. Dat kost één extra GET. cache: true op de call zetten helpt daar niet tegen: beide calls vertrekken vrijwel gelijktijdig, dus de in-memory cache is nog leeg als de tweede uitgaat — het levert alleen staleness op.

Initial Option Value hoort aan de PAGE PARAMETER te hangen, niet aan een page state die je in On Page Load vult — On Page Load draait in een addPostFrameCallback, dus ná de eerste build; de FormFieldController is dan al met de oude (lege) waarde geïnitialiseerd.

"Insert Before"/"Insert After" in het widget-tree-contextmenu is GEEN widgetkiezer — het dupliceert de buur-widget. Bevestigd 2026-09-03: "Insert Before" op een DropDown voegde een kopie van de dropdown erbóven toe (een derde categorie-dropdown), niet een dialoog om een nieuw widgettype te kiezen. Wil je er echt een ander widget (bv. een Text-label) tussen: dupliceer een bestaande Text en versleep 'm in de tree, of accepteer de volgorde. Verwijderen van de ongewenste kopie gaat gewoon via rechtsklik → "Remove Widget".

"Set from Variable": een bronregel die leeg/onzichtbaar blijft ná het uitklappen wordt zichtbaar door er met de muis overheen te bewegen. Bevestigd 2026-09-03, tientallen keren: na een klik op "App State"/"Page State"/"Widget State"/"Custom Functions" verschijnt wel de kop "Available Options" maar blijft de rij eronder leeg (0-hoogte/blanco). Eén hover op precies die rij (±18px onder "Available Options") rendert 'm alsnog. Dit is betrouwbaarder dan het oude advies "klik nog 2-3x" — dóórklikken klapt de sectie namelijk net zo vaak weer dicht. Werkwijze: uitklappen → hover → zoom op het gebied om de exacte y-positie te lezen → klikken. In combinatie met het zoekveld bovenin (typ de variabelenaam) is dit de snelste, meest voorspelbare route.

Een Local Page State-variabele HERNOEMEN behoudt alle bestaande bindings — gebruik dat als je een veld een andere betekenis wilt geven. Bevestigd 2026-09-03: createPlaatsID hernoemd naar createHorecagelegenheidNid; de Visibility-conditie van de verzendknop (!= null && != '') verwees daarna automatisch naar de nieuwe naam, zonder de conditie opnieuw op te bouwen. Dat scheelt precies het soort ConditionalBuilder-/Set-from-Variable-geklik dat elders in dit bestand als fragiel beschreven staat. Het paneel zit achter het 5e icoontje ("State Management") in de iconenrij van een geselecteerde pagina-root — níét achter het 3e (dat is Backend Query).

Een dropdown's "Initial Option Value" moet aan de PAGE PARAMETER hangen, niet aan een page state die je in On Page Load vult. FlutterFlow draait de On-Page-Load-keten in een addPostFrameCallback, dus ná de eerste build; de FormFieldController van de dropdown is dan al met de oude (lege) waarde geïnitialiseerd. Bind Initial Option Value dus rechtstreeks aan de parameter, en gebruik On Page Load alleen om de page state te vullen die je verderop nodig hebt (bv. voor een Visibility-conditie of een actie-argument).

FlutterFlowDropDown heeft in de builder-UI geen "Label Text"-property (alleen Hint Text), ook al ondersteunt de gegenereerde widget wel labelText. Zoeken op "label" in "Search properties..." levert alleen "Add Option Labels" en "Define Options Labels" op. Wil je een zichtbaar label boven een dropdown: een apart Text-widget, net als bij de andere velden.

"Remove Widget" op een Container haalt alleen de container weg en behoudt de kinderen — voor een heel blok gebruik je de prullenbak in de zwevende werkbalk. Bevestigd 2026-09-02: rechtsklik op een tree-node → "Remove Widget" op ContainerWaar verplaatste alle kinderen (dropdowns, tekstvelden) één niveau omhoog i.p.v. ze te verwijderen. Wil je de node inclusief subtree weg: rechtsklik de node, en klik in de zwevende icoontjesbalk die dan boven de rij verschijnt het rode prullenbak-icoon (uiterst rechts).

Een lijstwidget met paginering omzetten naar een grid (Replace) — volledig recept, bevestigd 2026-09-01 op HomeUitgaantabelKaartComponent. Rechtsklik → Replace weigert met "Can't replace a widget that has pagination enabled". De hele cyclus:

  1. Backend Query (3e icoontje) → EditEnable Infinite Scroll uit.
  2. De variabele die aan de paginering hing (hier page) wordt daardoor rood/ongeldig, en Confirm wordt geweigerd met de toast "Current query is invalid. Please fix." — je moet die binding dus eerst weghalen: rij openklappen → potlood bij Value → in de bronnenlijst het prullenbak-icoon linksboven ("Remove Variable") → er verschijnt een vrij tekstveld → typ een tijdelijke 0 → Confirm.
  3. Rechtsklik de node in de widget tree → Replace Widget → kies.
  4. Daarna alles terugzetten: Infinite Scroll weer aan, en het Value-icoontje naast de variabele → bron "Next Page Index ()" — dat is dezelfde bron die daarna weer als "Pagination - Next Page Number" in beeld komt. Let op wat een Replace stilletjes reset: Shrink Wrap valt terug op uit (zet 'm handmatig terug als het origineel 'm aan had), en een GridView krijgt er een Child Aspect Ratio bij die een ListView niet had.
  5. Meerkoloms maken van een lijst met paginering: de grid moet een BEGRENSDE hoogte krijgen, anders rendert hij niets. Volledig recept, live bewezen 2026-09-01 op Home (telefoon 411dp -> 1 kolom, tablet 1280dp -> 3 kolommen). Symptoom als je dit mist: PagedMasonryGridView toont een leeg vlak — geen kaarten, geen laadspinner, geen foutmelding, geen overflow. Niets in dart analyze, niets in logcat. De data komt gewoon binnen. Oorzaak: Home zet elk tabblad in een Column met "Scrollable" aan (genereert SingleChildScrollView), en daarmee is de hoogte oneindig. Een ListView met shrinkWrap: true overleeft dat, een masonry-grid niet. De TabBarView zelf zit al in een Expanded, dus de begrenzing is er wél — hij wordt alleen weggegooid. De drie wijzigingen (alle drie nodig):

    1. Op het component: ListViewStaggeredView (Replace; zie het pagination-recept hieronder), met Cross Axis Count als Responsive Value 1/1/2/3.
    2. Op datzelfde component: Shrink Wrap UIT. Met shrinkWrap aan blijft het leeg.
    3. Op de pagina, per tabblad: de omhullende Column op "Scrollable" uit, én het component daarin op Expanded (Wrap Widget → Flex → Expansion = Expanded). De grid scrollt voortaan zelf. Geverifieerd dat zowel "component direct in de TabBarView" als "Column(niet-scrollend) > Expanded > component" allebei werken; de tweede is in de builder het makkelijkst te klikken. GridView is hier géén alternatief: die bepaalt de celhoogte via childAspectRatio, en dat kan de vaste kaarthoogte (160/200/240/280 + 12px padding) principieel niet volgen — binnen één breekpuntband varieert de schermbreedte, dus elke waarde geeft óf overflow óf grote witruimte. StaggeredView geeft elk kind zijn eigen hoogte en heeft dat veld niet eens. StaggeredView ondersteunt gewoon paginering (PagedMasonryGridView + "Enable Infinite Scroll").

    Twee dialogen zijn wél te vergroten/verplaatsen — dat lost een hoop clipping op. Aanvulling op het doodlopende page-zoom-spoor hieronder:

    • De Backend Query-dialoog is groter te slepen aan zijn onderrand (het "———"-greepje onderaan). Dat was nodig om het Value-veld van een variabele te bereiken, dat er anders onder valt.
    • De "Set from Variable"-dialoog met een Responsive Value (4-traps if/else) toont standaard maar 2 van de 4 takken; sleep 'm aan zijn bovenrand omhoog, dan staan alle vier de waardevelden in beeld. Scrollen met het muiswiel werkt in beide gevallen niet — slepen wel.

    Rechterpaneel-clipping: klik op een sectiekop om die dicht te klappen — dat is betrouwbaarder dan de "Search properties..."-zoekbalk. Gevonden 2026-08-31: het paneel scrollt niet mee via muiswiel, en het zoekveld weigert regelmatig getypte tekst (de klik landt, de tekst komt nergens aan). Maar een klik op de tekst van een sectiekop ("Visibility", "Padding", "Alignment", "Button Text") klapt die sectie in en trekt alles eronder omhoog het zichtbare gebied in. Klap van onder naar boven in, dan blijven de coördinaten van de nog te klikken koppen kloppen. Wacht daarna 3-5s en maak een screenshot vóór je verder klikt — het paneel herberekent zijn layout, en een klik tijdens die herberekening landt op een heel ander veld (2026-08-31 opende zo per ongeluk een "Theme Text Style"-dropdown en een "Set from Variable"- dialoog; beide met Escape/X ongedaan te maken zonder schade).

    Kleurdialoog ("Choose a Color") opent traag en soms pas bij de tweede klik — batch nooit blind "klik swatch → klik kleur". Bevestigd 2026-08-31 op tientallen swatches: soms opent hij binnen 4s, soms pas na 10s, en soms doet de eerste klik niets. Twee klikken achter elkaar op de swatch is óók fout — dan opent en sluit hij weer. Werkende routine: klik swatch → wacht 8-10s → screenshot → pas dán op de themakleur klikken → sluiten met de X → verifiëren met een zoom op het veld. De lijst "Theme Colors" onderin de dialoog is de plek die een token bindt (FlutterFlowTheme.of(context).<token>); het hexveld erboven maakt juist weer een hardcoded kleur.

    Een rode Issues-teller blokkeert élke export — en een fout aanklikken navigeert je rechtstreeks naar de widget die hem veroorzaakt. Dat laatste is de snelste diagnose die er is en stond hier nog niet in: klik de rode teller rechtsboven → tabblad "Errors" → klik een regel → de builder springt naar de juiste pagina én opent het bijbehorende paneel (bij een actie zelfs de Action Flow Editor met de betreffende actie geselecteerd). Gebruik dit altijd vóór je zelf gaat zoeken. Let op: die teller telt fouten van álle sessies bij elkaar op, dus een geblokkeerde export is vaak niet jouw schuld — check eerst wíe de fout veroorzaakt voordat je in je eigen werk gaat spitten.

    Direct ná een zware builder-operatie (bv. Duplicate Page van een grote pagina) faalt export-code een paar keer met Unexpected error from the server — wacht even en probeer opnieuw. Bevestigd 2026-09-04: 3 aanroepen achter elkaar faalden, een vierde een paar minuten later gaf gewoon "All done!". Het Issues-paneel gaf geen blokkerende fout. De backend is dan nog aan het verwerken; ga niet in je eigen wijziging zoeken.

    Een mislukte export kan ook gewoon een serverhikje zijn — probeer 'm één keer opnieuw vóór je gaat diagnosticeren. Bevestigd 2026-09-04: flutterflow export-code gaf Body: Error generating code for the project. Make sure there are no project errors. + Unexpected error from the server, terwijl het Issues-paneel alleen de twee bekende niet-blokkerende Property Override-fouten toonde en een export van een halfuur eerder met exact dezelfde foutenlijst prima slaagde. Een identieke tweede aanroep, direct erna, gaf gewoon "All done!". Ga dus niet meteen op zoek naar wat je zojuist gewijzigd hebt.

    Nuance (2026-09-03, gemeten): NIET elke error blokkeert de export — alleen sommige soorten. De kop hierboven is te absoluut. Concreet gemeten op dit project met 3 errors in de lijst:

    • Blokkeert wél: API Action — Variable value configured incorrectly for API call (een Backend Call in een actieketen met ongebonden variabelen, hier session_name/sessid op een On-Page-Load-call). Zolang die erin staat faalt élke export met de generieke Status: 400 / Error generating code for the project.
    • Blokkeert niet: Property Override — Invalid API call configuration en Property Override — return type mismatch op een dropdown. Met alléén die twee in de lijst exporteert het project gewoon. Praktisch gevolg: ga bij een 400 niet alle errors afwerken, maar klik ze aan en zoek de actie-gerelateerde eruit — dat is meestal de enige die je moet fixen. En andersom: "er staan nog errors" betekent niet automatisch dat je niet kunt exporteren; test het gewoon. Verwarrende bijkomstigheid: FlutterFlow's eigen preview/Test Mode is veel toleranter en draait vaak door terwijl export-code weigert — "de build doet het toch gewoon" is dus geen bewijs dat de teller onschuldig is.

    Een pagina die alleen via een page-parameter bereikbaar is, test je zonder de builder met een profile-build + --route. Recept dat 2026-09-05 in één keer werkte voor HorecagelegenhedenOverzichtCopy3:

    cd /tmp/ff-checkN && fvm flutter run --profile -d <device> \
      --route "/horecagelegenhedenOverzichtCopy3?plaats=25434"
    

    (25434 = Arnhem, een plaats met content in vrijwel elke categorie.) Vergeet niet eerst .fvmrc, .fvm/ en pubspec.lock naar de exportmap te kopiëren — zonder die drie bouwt hij niet, zie de notitie daarover elders. Deze route is browserloos en dus juist geschikt wanneer Bob niet achter zijn scherm zit. Handig bij het aflezen: mcp__android__get_ui_tree geeft de kaarten met hun volledige tekst als desc, waardoor je het aantal treffers exact kunt tellen en tegen de API kunt naleggen — betrouwbaarder dan tellen op een screenshot. Let op: het toetsenbord dekt de lijst af en keyevent 111 sluit 'm niet als het veld nog focus heeft; keyevent 4 (back) werkt wel.

    Categorievelden uit flutterflowmobiel_establishments.json zijn LIJSTEN, geen strings. Bevestigd 2026-09-05: "categorie": ["Italiaans restaurant", "Pizzeria"]. Elke functie die daarop filtert of er opties uit opbouwt, moet de binnenlijst plat slaan; een .toString() op het hele veld levert [Pizzeria] op en matcht dan nergens op. Reken er verder op dat de view echte duplicaten bevat (zelfde titel, verschillende nid en categorie-set) — dat is data, geen app-bug.

    Rechterpaneel-tekstvelden: triple_click + typen werkt, left_click + ctrl+a + typen niet — en commit met een klik ergens neutraals, niet met Tab. Bevestigd 2026-09-05 op een TextField's Hint Text: ctrl+a gevolgd door typen liet de oude waarde onaangeroerd staan, triple_click gevolgd door typen verving 'm meteen. Daarna Tab drukken draaide de waarde terug; klikken op een sectiekop in hetzelfde paneel legde 'm wél vast. (Dit nuanceert de oudere notitie dat typen in het rechterpaneel voor Claude "structureel onmogelijk" is — met de juiste combinatie lukt het gewoon.)

    Een breedte op double.infinity zetten hoeft niet getypt te worden: in het Width-veld staat rechts een ∞-knopje dat het veld op inf zet. Zelfde geldt voor Height. Scheelt het gevecht met numerieke invoervelden.

    FlutterFlow's On Change-trigger op een TextField krijgt automatisch een EasyDebounce van 2000 ms, en die is niet instelbaar. Bevestigd 2026-09-05: geen debounce-eigenschap op het widget (zoeken op "debounce" in "Search properties..." geeft niets), en noch de trigger in de Action Flow Editor noch het "⋮"-menu op de actie-node biedt de optie. Bouw je een live zoekveld dat op een lokale lijst filtert, reken dan op ~2 s vertraging na de laatste toetsaanslag. Wil je sneller: laat de widget die je filtert rechtstreeks _model.textControllerN.text lezen én zorg voor een andere rebuild-trigger.

    Bind een verplicht (non-nullable) String-argument van een Custom Function NOOIT aan een nullable Page State — dat genereert een ! en crasht bij de eerste build. Bevestigd 2026-09-05 (filterHorecagelegenheden op HorecagelegenhedenOverzichtCopy3): het argument zoekterm (type String) gekoppeld aan de page state zoektermActiviteiten (String?, initialiseert op null) leverde _model.zoektermActiviteiten! op → Null check operator used on a null value zodra de tab bouwt. dart analyze ziet dit niet (het is geldige Dart), dus een schone analyse is hier geen bewijs. Fix: bind aan Widget State van het invoerveld zelf (TextField 1) — dat geeft _model.textController1.text, non-nullable. De page state blijft wel nodig als rebuild-trigger: zijn safeSetState is wat de lijst opnieuw laat filteren.

    Het "Local Page State Variables"-paneel (pagina-root → 5e icoontje) slaat wijzigingen structureel NIET op — ook niet in Bob's eigen browser. Bevestigd 2026-09-05, vier verschillende bewerkingen: een veld hernoemen, Nullable uitvinken, Initial Field Value vullen, en de "Default Variable Value" in de Set-Variable-dialoog. Alle vier tonen de nieuwe waarde in de UI, de header meldt "Synced", en een verse export toont onveranderd de oude staat. Reken er dus op dat een page-state-veld zijn oorspronkelijke naam en nullability houdt; plan er geen opschoning omheen. (Dit spreekt de eerdere notitie over createPlaatsIDcreateHorecagelegenheidNid tegen — die rename werkte in 2026-09-03 wél; blijkbaar niet betrouwbaar reproduceerbaar.)

    Het zoekveld in de "Set Variable"-dialoog filtert over álle bronnen, ook Widget State en Custom Functions. Bevestigd 2026-09-05: typ gewoon TextField 1 of filterHoreca en de lijst reduceert tot die ene bron met de optie er direct onder. Veel sneller en betrouwbaarder dan de bron uitklappen en de bekende hover-truc gebruiken om een leeg gerenderde rij zichtbaar te maken. Let op: de eerste keer typen landt vaak niet (het veld krijgt pas focus na de klik) — klik, typ, en verifieer met een zoom; reken standaard op 2 pogingen per invoerveld in deze dialoog.

    "Set Function Arguments" met 5 argumenten is wél volledig bereikbaar — anders dan het evenementCreate-geval verderop. Twee dingen samen: sleep de onderrand van de dialoog omlaag (groeit ±70px per sleep), en klap elk afgehandeld argument in via zijn chevron, dan schuift het volgende in beeld. Zo zijn op 2026-09-05 vijf keer achtereen alle 5 argumenten gezet. Vergeet na Confirm in de dialoog niet de aparte Save-knop in het rechterpaneel — zonder die tweede klik wordt er niets weggeschreven.

    Het tree-zoekveld ("Search for widget...") is de betrouwbaarste navigatie bij herhaald werk op gelijksoortige widgets. Typ bv. StaggeredView en de boom reduceert tot alleen die nodes, in documentvolgorde (dus tabvolgorde) — dat omzeilt zowel het niet-scrollende boompaneel als de bekende klik-offset. Zonder dit filter sprong de selectie tijdens deze sessie meermaals naar de pagina-root (rechterpaneel toont dan opeens "Local Page State Variables") — onschadelijk, maar je moet het wel opmerken vóór je verderklikt.

    Een Custom-Action-argument van het type List<String> dat als fout "argument X is not set properly" gemeld wordt: klap in de Inline Function-dialoog "Return Type (Optional)" open en druk dan pas Confirm. Bevestigd 2026-08-31 op evenementCreate's categorieTids: de expressie was gewoon <String>[] en de dialoog meldde "No Errors", maar de fout bleef staan tot ik die sectie één keer had opengeklapt en opnieuw bevestigd — daarna viel de teller van 6 naar 5. Het openklappen lijkt het wegschrijven van het return-type te forceren. Werkt niet als er nog helemaal géén binding staat (waarde UNSET): een verse Inline Function aanmaken lukt in de UI, maar Confirm schrijft 'm niet weg (5 pogingen, waarde bleef UNSET) — zelfde "ziet-er-opgeslagen-uit"-patroon als elders. Dan is de knop/actie weggooien en opnieuw opbouwen sneller dan doorprobéren.

    Een slider-met-getalveld (Border Radius, Opacity, Elevation …) toont ná het typen vaak nog de OUDE waarde in het getalveld terwijl de slider al klopt — geloof de slider, niet het getal, en verifieer met een export. Bevestigd 2026-09-03 op tagCategorieComponent's Border Radius: veld bleef 12 tonen (ook na de node te herselecteren), slider stond links op 0, en de verse export gaf BorderRadius.circular(0.0). Niet nog een keer typen dus — dat riskeert juist een verkeerde waarde.

    Tekstschaduw van een Text/AutoSizeText verwijderen: widget selecteren → sectie "Shadows" onderaan Text Properties → klik op de rij "Shadow 1" (niet op het chevron) → er verschijnt rechts in diezelfde rij een rode link "Remove" → klikken. Daarna staat er alleen nog "+ Add Shadow". Werkt betrouwbaar, 6x achter elkaar zonder falen.

    De "Search pages or components..."-lijst filtert trager dan je klikt — wacht op een screenshot vóór je een zoekresultaat aanklikt. Bevestigd 2026-08-31: na het typen van een nieuwe zoekterm staan de resultaten van de vórige zoekopdracht er nog even, en een klik op "de eerste regel" landt dan op de verkeerde pagina (in dit geval kanwegHomeCopy i.p.v. PUitgaanPage). Geen schade zolang je daarna de paginanaam in het rechterpaneel of de URL controleert — doe dat dus altijd vóór je iets wijzigt.

    Een widget-kleur aan een thema-token binden (i.p.v. een hardcoded hex): klik het kleine kleurblokje vóór de kleurnaam, niet de tekst ernaast. Klikken op de tekst ("Primary", of een hex) maakt er een bewerkbaar tekstveld van — daar een hex intypen levert weer een hardcoded kleur op, precies wat P1-33 probeert weg te werken. Klikken op het blokje opent "Choose a Color" met onderaan een "Theme Colors"-lijst (Primary/Secondary/Tertiary/Alternate/…); één klik op een regel daar bindt aan het token en genereert FlutterFlowTheme.of(context).<token>.

    Snelste route naar een specifiek widget op een pagina: de zoekbalken, niet scrollen/klikken in de boom. Drie zoekvelden werken betrouwbaar en besparen veel misklikken (zie de bekende tree-coördinaat-instabiliteit hieronder): (1) "Search pages or components..." bovenin het linkerpaneel — pagina's én componenten, wisselt binnen de app (±15s) i.p.v. een volledige URL-navigatie (±35s herlaad van de hele builder); (2) "Search for widget..." in het Widget-Tree-paneel — typ bv. Button en de boom filtert tot alleen die nodes, ongeacht hoe diep ze genest zijn; (3) "Search properties..." rechtsboven — filtert het rechterpaneel tot bv. alleen "Fill Color", waardoor die eigenschap bovenaan komt te staan i.p.v. onderaan tegen de vensterrand. Let op: na het selecteren van een tree-node duurt het 3-5s voor het rechterpaneel bruikbaar is; een klik/typ-actie die te snel volgt komt niet aan (stil, geen foutmelding) — altijd verifiëren met een zoom-screenshot vóór je verdergaat.

    Een List-typed custom-action-argument (List<String> e.d.) mag niet op "Unset" blijven staan — dat blokkeert de hele export. Bevestigd 2026-08-31 (evenementCreate): ook als het argument in Dart nullable is (List<String>?), meldt het Issues-paneel Custom action argument "X" is not set properly, en flutterflow export-code faalt daarna met een generieke Status: 400 / Error generating code for the project die de oorzaak níét noemt. Optionele String-argumenten mogen wél gewoon Unset blijven; alleen List-types niet. Oplossing: Value-potlood → Set Variable → Inline Function → Expression <String>[] → Check Errors → Confirm. Bij een onverklaarbare 400 op een export dus altijd eerst het Issues-paneel openen vóór je in de code gaat zoeken.

    Het niet-scrollende rechterpaneel is NIET te omzeilen met page-zoom — niet opnieuw proberen. Getest 2026-08-31 via javascript_tool: document.body.style.zoom='0.75' wordt wel toegepast (alles wordt zichtbaar kleiner), maar Flutter Web relayout niet — het canvas houdt zijn logische afmetingen, dus je ziet exact dezelfde hoeveelheid content. Ook flutter-view handmatig groter maken via style.width/style.height plus een gedispatcht resize-event verandert niets. Wat wél werkt om iets onderin het paneel te bereiken: de secties erboven één voor één inklappen via hun chevron — maar het paneel accepteert maar één inklap-klik per tool-aanroep (meerdere kliks in één browser_batch laten alleen de eerste landen), dus reken op één round-trip per sectie. Zit het doelveld daarna nog steeds onder de rand: overdragen aan Bob, in zijn browser staat het gewoon in beeld.

    Rechterpaneel-clipping: gebruik de "Search properties..."-zoekbalk i.p.v. scrollen. Het rechterpaneel (Widget Tree → property-editor) scrollt via browser-automation vaak niet (muiswiel-events lijken geen effect te hebben op die specifieke sectie) — de bekende breedte-clipping (zie hieronder) geldt dus ook verticaal. Werkende workaround: het zoekveld bovenaan het paneel ("Search properties...") filtert op eigenschapsnaam (bv. "blur", "offset", "radius", "padding", "shadow") en toont dan alléén die eigenschap, ongeacht scrollpositie — vrijwel altijd binnen het zichtbare/klikbare gebied. Bij een samengestelde eigenschap met 2 velden naast elkaar (bv. Offset X/Y) kan het tweede veld nog steeds net buiten beeld vallen; een Tab-naar-volgend-veld-workaround is niet betrouwbaar gebleken (de waarde bleef soms op de oude staan zonder foutmelding, 2026-08-06 bevestigd op PUitgaanSliderKaartComponent's schaduw-offset). Voor sliders zonder zichtbaar numeriek veld (bv. "Uniform Radius/Padding" na het omzetten van per-hoek naar uniform): klikpositie op de track geeft alleen een grove benadering, geen exacte waarde — prima voor "ruwweg kleiner/groter", niet voor een precieze doelwaarde.

    • Na een edit altijd verifiëren met een screenshot van het gefilterde veld — een schijnbaar geslaagde Ctrl+A + typen + Return/Tab bleek meermaals niet vast te houden. Werkte betrouwbaarder: triple-click op het veld → Delete → typen (i.p.v. Ctrl+A → typen).
    • Zekerste verificatie: "View Code"-paneel, maar bereik dat via directe navigatie naar https://app.flutterflow.io/code/<project>?component=<Naam> (of ?page=<Naam>) i.p.v. te klikken op het </>-icoon rechtsboven — die iconenrij verschuift afhankelijk van app-state (een commit-icoon verschijnt/verdwijnt), waardoor een vast coördinaat soms een heel ander icoon raakt. Bevestigd 2026-08-06: 2x per ongeluk een "Create Commit"-dialoog geopend, 1x een "Make Project Public"-dialoog (met een Make Public-knop — nooit klikken), 1x de app-preview. Geen van alle bevestigd/doorgeklikt, maar wel tijdverlies — klik in die iconenrij rechtsboven nooit blind op een onthouden coördinaat, altijd eerst een screenshot ná de vorige actie. In de code-view zelf: gewoon muiswiel-scrollen werkt daar evenmin betrouwbaar, gebruik Ctrl+F (opent een echte browser-find-balk, werkt wel).
      • Update 2026-08-07: na directe navigatie naar de code-URL toont het middenpaneel soms alleen de kleine component-preview-thumbnail rechtsonder, geen code — klik op die thumbnail (niet op de "Widget"/"Model"-toggle erboven) om de code-editor daadwerkelijk te tonen; kost soms 2 pogingen na een page-reload. Eenmaal in de editor: Ctrl+F opent hier een Monaco-stijl in-editor zoekbalk (geen browser-native find), en die zoekbalk toonde bij mij geen bruikbare matchteller (mogelijk zelf ook geclipt) — voor het vinden van een specifieke regel werkte Ctrl+G ("Go to Line") betrouwbaarder: typ een regelnummer (desnoods een schatting op basis van de zichtbare structuur) + Enter, springt direct daarheen. De editor is read-only (typen geeft een "Cannot edit"-tooltip) — geen risico op per ongeluk wijzigen.

    Geneste "Set Variable"-dialoog lijkt vast te zitten. Bij een conditie (ConditionalBuilder, Visibility → Conditional) op een niet-triviaal type (JSON Path, API-response-veld, List<DataType> function-argument) opent een tweede dialoog bovenop de eerste; Confirm/Cancel reageren soms niet zichtbaar. Twee oorzaken, in volgorde van proberen:

    1. Viewport-clipping (meest voorkomend) — knoppen renderen buiten het zichtbare canvas. Sleep de dialoog omhoog via het handvat bovenin naar een hogere y-positie; de knoppen worden dan zichtbaar en werken gewoon.
    2. Echt bevroren pagina (alle clicks doen niets) — de widget-wrap staat al server-side, de conditie-edit niet. Herlaad de pagina (navigate naar dezelfde URL); wrap blijft staan, conditie moet opnieuw.
    3. Kortere weg om dit te vermijden: operator "Is Set" i.p.v. "Not Equal To" + lege string — geen Second Value nodig, dus geen tweede dialoog.
    4. Loopt dit na 1-2 pogingen (incl. reload) nog vast: kost dan meer tijd dan Bob het zelf kan doen — meld concreet (component, exacte stappen) en vraag het aan hem.

    Component Name kan per ongeluk overschreven worden. Concreet coördinaat-risico (bevestigd 2026-08-31, Claude, 1x echt gebeurd): bij een geselecteerd widget staat "Search properties..." op ±y=116, maar staat er een component-root geselecteerd, dan zit op vrijwel exact diezelfde plek het Component Name-veld (±y=123). Landt een tree-klik niet (het bekende selectie-verlies), dan typ je je zoekterm dus regelrecht in de componentnaam — EvenementHorecagelegenheid werd zo hernoemd naar color, zichtbaar aan de URL die omsloeg naar ?tab=widgetTree&component=color. Herstel is volledig en ongevaarlijk: component-root in de tree selecteren → naamveld → triple-click → juiste naam typen → Return; een verse export laat daarna weer de originele bestands-, map- én klassenaam zien (FlutterFlow leidt die allemaal af van de componentnaam, er blijft niets achter). Herhaald opgetreden 2026-09-03 op tagCategorieComponent: een klik op "Search properties..." landde op het naamveld en de zoekterm "radius" maakte er tagCategorieCompradiusonent van. Herstel kostte twee handelingen en geen data. Vuistregel die dit definitief voorkomt: klik eerst het kind-widget in de tree aan (dan is de component-root niet meer geselecteerd) en typ pas daarna in het rechterpaneel. Voorkomen: vóór je in het rechterpaneel typt altijd eerst een zoom op de paneelkop (regio ±[970,30,1148,130]) om te bevestigen dát het gewenste widget geselecteerd is — de widgetnaam staat daar bovenaan. Vlak na paginanavigatie kan een klik bedoeld voor "Search properties..." op het Component Name-veld landen (focus/z-order race), en typen hernoemt dan stilletjes het component. Zelfde risico bij een widget-tree zoekactie die per ongeluk double-click-to-rename triggert i.p.v. navigeren — druk direct Escape om te herstellen. Mitigatie: na elke click-before-type eerst een screenshot om focus te bevestigen, zeker vlak na navigatie. Herstel: rechtsklik component in zoekresultaten → "Rename Component".

    Rechterpaneel kan te breed zijn voor de viewport. Sommige controls (Expansion segmented control, Visibility → Conditional expression-builder, maar ook simpele checkboxen zoals "Show Empty List Widget" op een Carousel/ListView) renderen soms deels buiten beeld — geen resize_window-probleem (niet zelf resizen, zie boven). Bevestigd 2026-08-04: dit blijft optreden zelfs nadat Bob zijn eigen venster al vergroot had — de FlutterFlow-app zelf lijkt de extra breedte niet te gebruiken (real window 1970px, maar bruikbare schermafbeelding/klikbare ruimte bleef begrensd tot ~1176px; de JS-laag rapporteert wel de volle vensterbreedte, dus dit zit in hoe Flutter Web rendert/schaalt, niet in het venster zelf). Geen DOM/accessibility tree beschikbaar (canvas-rendering) — find en read_page werken hier niet, alleen coördinaat-gebaseerd klikken. Geprobeerd en zonder succes: direct klikken op meerdere x-posities, klikken + Space-toets, horizontaal scrollen. Na 1-2 bevestigde pogingen stoppen en aan Bob vragen — geef het exacte pad (component, tree-node, veldnaam) zodat hij het in seconden kan doen.

    • Specifiek bevestigd structureel (2026-08-05) voor de "Show Empty List Widget"-checkbox op een Carousel: dit is geen per-component toeval maar een systematische blokkade van Claude's browser-automation-viewport — opgetreden op vier verschillende componenten (HomeUitgaanSliderComponent, PUitgaanSliderComponent, EvenementComponent, horecagelegenheidCurrent, allemaal Carousel). Niet meer opnieuw proberen per component — deze checkbox specifiek is voor Claude via browser-automation niet bereikbaar, ongeacht welk component. Verzamel in plaats daarvan de volledige lijst van componenten die de fix nodig hebben en geef die in één keer aan Bob (elk 10 sec in zijn eigen browser, geen viewport-beperking daar).

    Patroon: lege/ontbrekende afbeeldings-URL laat de app crashen. CachedNetworkImage gooit een synchrone ArgumentError bij het bouwen van de widget als imageUrl een lege string is — dit gebeurt vóórdat er ooit een netwerkverzoek is, dus errorWidget/"Show Error Image on Failure" vangt dit niet (dat vangt alleen échte laadfouten zoals 404's). Werkende fix:

    1. Rechtsklik het Image-widget → Wrap Widget (Ctrl+B)ConditionalBuilder.
    2. IF-conditie: Conditions → Single Condition → First Value = de exacte expressie waar de Image's Path-property al aan gebonden was → operator "Is Set".
    3. THEN-tak: de bestaande Image (blijft staan na de wrap).
    4. ELSE-tak: rechtsklik → Insert Widget → Icon → "image not supported" → eerste Material-resultaat.
    5. Simpeler alternatief indien van toepassing: de "Default Variable Value"-toggle op een "Set from Variable"-binding substitueert al bij zowel null als lege string (in-builder tooltip: "if the resulting value is null or empty") — géén ConditionalBuilder nodig. Werkt hier niet voor Image-widgets met Image Type: Network zolang er geen gehoste fallback-afbeeldings-URL bestaat in dit project. Komt die er ooit (bv. default-logo op FlutterFlow-CDN/Drupal-server): gebruik dan Default Variable Value + "Show Error Image on Failure" — sneller te bouwen dan ConditionalBuilder.

    Laad-placeholder voor CachedNetworkImage (trage/lege witte vlek tijdens laden, zie TASKS.md P2-12): via de "Use Blur Hash"-toggle, niet via een handmatige Shimmer/Container-wrap. FlutterFlow's Image-widget heeft geen los "placeholder:"-veld in de rechterpaneel-UI — de enige ingebouwde route is: Image-widget selecteren → Search properties → "blur" → "Use Blur Hash" aanzetten → een "Blur Hash String"-tekstveld verschijnt. Dit project heeft geen per-afbeelding blurhash-data uit Drupal, dus een vaste, algemene hash volstaat als neutrale grijze placeholder (geen per-foto accurate blur, gewoon een nette laad-vlek): L6PZfSi_.AyE_3t7t7R**0o#DgR4. Genereert automatisch OctoImage(placeholderBuilder: (_) => Image(image: BlurHashImage('...')), image: CachedNetworkImageProvider(...)) — geen widget-tree-wijziging nodig. Valkuil: de eerste klik/typing in het "Blur Hash String"-veld registreert soms niet (blijft leeg na Tab of na een andere actie ertussen) — zelfde "ziet er opgeslagen uit, was het niet"-patroon als elders in dit bestand. Altijd verifiëren met een zoom-screenshot op het veld zelf (niet alleen het canvas) vóór je verdergaat, en met een verse export dat de string niet leeg bleef.

    "Is Set and Not Empty" is niet altijd beschikbaar als operator — hangt af van hóe de First Value gebonden is, niet alleen van het onderliggende veldtype. Bevestigd 2026-08-21 (Claude, ~45 min vastgelopen op HomeUitgaantabelKaartComponent's $.logo-guard, zie TASKS.md P1-24): een First Value gebonden via JSON Path op een raw loop-item (bv. evenementenItem uit "Generate Dynamic Children", geen gekoppeld Custom Data Type) krijgt in de builder-UI het type-label "Json", en voor dat type biedt de operator-lijst alleen Equal To/Not Equal To/Is Set/Is Not Set"Is Set and Not Empty" ontbreekt gewoon, ook al is de onderliggende waarde gewoon een String. Uitgeprobeerd en allemaal doodgelopen: (1) een 2e AND-conditie met != '' toevoegen — de "Second Value"-picker biedt voor een Json-typed vergelijking geen vrij tekstveld, alleen een variabele-bronkiezer (geen "literal"-optie, ook niet via de zoekbalk-truc); (2) de First Value ombuigen naar "Predefined Path" i.p.v. "JSON Path" in de hoop op een String-type — werkt alleen als de bron een echt Custom Data Type heeft (raw JSON-loop-items hebben dat per definitie niet, dus de path-kiezer toont dan gewoon niets); (3) een "empty"/"length"-transform zoeken in de 2e "Available Options"-dropdown (naar analogie van de bekende List Contains Item-truc) — alleen "To Data Type" (custom models) en "No Further Changes" beschikbaar. Vuistregel: "Is Set and Not Empty" werkt betrouwbaar op een directe component-parameter die zelf al als String gedeclareerd is (zie het bekende P0-8-recept — dat blijft gewoon correct), maar niet op een JSON-Path-binding tegen een raw/ongetypeerd loop-item. Bij zo'n loop-item-guard: of eerst het component-parameter zelf String-typeren (als het als parameter doorgegeven wordt aan een sub-component), of het probleem als "Is Set" (null-check alleen) accepteren en de restkans op een lege-string- .toString() → letterlijke "null"-tekst als cosmetisch, lager- risico restpunt behandelen (P1-1's precedent) i.p.v. een crash.

    Update 2026-08-21 (sessie 43): het ontbreken van een letterlijk tekstveld voor "Second Value" hierboven is specifiek voor een Json-typed First Value — voor een directe String/Image-Path- component-parameter bestaat dat veld wél, alleen verstopt. Bevestigd werkend op PUitgaanSliderKaartComponent's logo-parameter (Image Path-type): Single Condition → First Value = de rauwe component- parameter → operator "Not Equal To"Second Value: klik niet op het "+"/"—"-icoon (toggelt alleen open/dicht zonder invoerveld te tonen), maar op de tekst-hint zelf (bv. "Is Set") — dat verandert de rij naar een editable staat; klik daarna nogmaals op het "+"-icoon ernaast → nu verschijnt een echt "Value"-tekstveld met placeholder "Unset". Typ een willekeurig teken en verwijder het weer (bv. "x" + Backspace) — dit forceert het veld om als letterlijke lege string te registreren (placeholder-tekst springt om naar "[Empty String]" i.p.v. terug te vallen op "Unset"/null). Resultaat: if (widget!.logo != '') { ... } — exact de lege-string-guard die eerder onbereikbaar leek. Geldt niet voor het Json-Path-geval hierboven (HomeUitgaantabelKaartComponent's $.logo) — dat blijft een echte FlutterFlow-beperking, zie TASKS.md P1-24.

    Patroon: "Is Set and Not Empty"-conditie tegen een Default-Value/valueOrDefault-gebonden veld is een no-op — en kan een crash verbergen. Bevestigd 2026-08-07 (evenement_horecagelegenheid_widget.dart, zie TASKS.md P1-15): gegenereerde code als if (valueOrDefault<String>(veld, 'placeholder') != null && valueOrDefault<String>(veld, 'placeholder') != '') is altijd waar, omdat valueOrDefault bij een lege/ontbrekende bron juist de placeholder-tekst teruggeeft (nooit null/''). Gevolg: een widget die eigenlijk verborgen zou moeten zijn bij ontbrekende data blijft zichtbaar mét de placeholder-tekst — en als die widget een onTap/actie heeft die de (placeholder-)waarde gebruikt (bv. launchURL(valueOrDefault(...)) op een social-media-/website-knop), crasht de app bij tikken i.p.v. gewoon niets te doen. Fix: bind de Visibility/If-conditie's First Value niet aan een veld waar al een Default Value op zit, maar rechtstreeks aan de rauwe API-response- expressie (dezelfde JSON Path als de widget's eigen Path-property, vóór een eventuele Default Value-substitutie) met operator "Is Set" — zelfde recept als de CachedNetworkImage-fix hierboven. Check dit patroon bij elke knop/link die een API-veld gebruikt, niet alleen bij de al bekende P1-6-lijst met lelijke placeholder-tekst — die twee problemen (lelijke tekst vs. crash-bij-tikken) hebben dezelfde bron maar zijn niet automatisch allebei opgelost met alleen een betere Default Value-tekst.

    Custom-Function met meerdere argumenten in een Set-Variable-actie: tweede argument bevriest de pagina. Bij het configureren van een multi-argument custom function (bv. gemeenteNaamById(lijst, id)) als Set-Variable-waarde: het eerste argument instellen via de "Search variables..." → categorie-expand → item-klik-flow werkt betrouwbaar. Zodra je daarna probeert het tweede argument in te stellen (klik op de argument-dropdown-selector, bv. van "lijst" naar "id"), kan de hele pagina volledig bevriezen (alle clicks doen niets meer, geen enkele visuele terugkoppeling) — bevestigd reproduceerbaar 2026-08-05 op gemeenteNaamById, NIET opgetreden bij hetzelfde patroon op provincieNaamById een moment eerder (niet 100% deterministisch, maar bij deze functie 3x achter elkaar gereproduceerd). Reload lost dit niet volledig op zoals bij het bekende "echt bevroren pagina"-patroon hierboven: de Custom-Function-keuze zelf (bv. welke functie, welk App-State-veld het target is) overleeft een reload wél, maar het al ingestelde eerste argument (bv. lijst) gaat weer naar "UNSET" terug — dus geen gratis doorstart, elke reload kost een herhaling van stap 1.

    • Niet blijven proberen na 2-3 pogingen (elke poging = volledige page-reload + component heropenen + Actions-tab + veld heropenen, kost al snel 10+ tool-calls) — meld concreet aan Bob welke twee argumenten hij moet zetten en op welk veld, dat kost hem in zijn eigen browser seconden.
    • Voorbeeld hoe dat er dan uitziet (2026-08-05, SelectStateDropDownComponentDropDownGemeente → Actions → On Selected → Action 1 → Set Fields → gemeenteSelectNaam): de functie gemeenteNaamById staat al goed gekozen (herkenbaar aan rode "gemeenteN…" tekst i.p.v. "Unset" in de Set Fields-lijst), nog toe te voegen: argument lijst = App State gemeentelijst, argument id = App State gemeenteSelectId, dan Confirm.
    • Zelfde bevroren-Confirm-patroon ook bevestigd op een Text-widget's If/Then/Else-conditie (niet alleen Custom-Function-argumenten) — 2026-08-05 op EventCurrent's AppBar-Row, in totaal 5x bevroren bij het klikken op "Confirm" (soms al bij de binnenste Single-Condition-Confirm, soms pas bij de outer Confirm) ná het instellen van een Single Condition (Is Set and Not Empty). Precies dezelfde stappen lukten wél zonder freeze op HeaderButtonsComponent, én lukten wél voor het vergelijkbare Gemeente-Custom-Function- argumentenpunt na een computer-restart — dus dit is niet louter algemene omgevingsinstabiliteit die met een restart oplost. Een computer-restart tussen pogingen 2 en 3 loste dit specifieke geval niet op (2x nieuwe freeze ná restart, identiek patroon). Blijft onverklaard waarom dit specifieke widget/actie op dit specifieke component structureel vaker vastloopt dan elders — mogelijk iets aan de AppBar-Row-context van EventCurrent zelf. Na 5x: gestopt met proberen, definitief overgedragen aan Bob (zie TASKS.md P0-3). Vuistregel blijft: 1-2 pogingen (incl. 1 reload), dan overdragen met exacte stappen — niet blijven proberen.

    Widget kopiëren/plakken werkt NIET via browser-automation — dood spoor, niet opnieuw proberen. Bevestigd 2026-09-01: "Copy Widget" + "Paste Widget" uit het ⌘+K-palet, én de sneltoetsen Ctrl+C/Ctrl+V op een geselecteerde tree-node, schrijven allebei niets weg — niet tussen twee pagina's en ook niet binnen dezelfde pagina. Het palet-item "Paste Widget" highlight wel bij hover, maar zowel een klik als Enter laat de doel-node onveranderd (geen foutmelding, header blijft op "Synced"). Gevolg: een kapotte tab herstellen door het intacte blok van een zusterpagina te kopiëren is geen optie — je moet ter plekke opnieuw opbouwen (Backend Query → Generate Dynamic Children → parameters binden → actie). Wat wél werkt: rechtsklik op een component in de "Search pages or components..."-lijst → "Duplicate Component" (maakt een volledige, werkende kopie inclusief Backend Query en pagination), en "Copy Action Chain"/"Paste Action(s)" voor losse actieketens (zie de valkuil daarbij elders in dit bestand).

    Een widget VERPLAATSEN lukt wél — sleep de rij in de Widget Tree, en een drop op een container-rij voegt hem er altijd als EERSTE kind in. Bevestigd 2026-09-03 (P1-36, HorecagelegenheidoverzichtKaart): een complete subtree (Container > Column met Generate Dynamic Children > groene labels) verhuisde in één sleepactie van de Stack naar de rechterkolom, mét behoud van de dynamische kinderen, de .take(3) en de tekstbinding. Dat is dus de werkende tegenhanger van het copy/paste-dode- spoor hierboven: kopiëren kan niet, verplaatsen wel.

    • Het insert-gedrag is deterministisch: altijd positie 0. Een drop op de onder- of bovenhelft van een rij maakt geen verschil, en een drop in de lege ruimte ónder de boom doet niets. Wil je een node dus als laatste kind: laat 'm niet slepen naar de plek waar hij moet komen, maar drop alle siblings ná hem opnieuw op de ouder-rij, in omgekeerde volgorde. Van [C, A, B] naar [A, B, C] is: drop B op de ouder → [B, C, A], drop A op de ouder → [A, B, C]. Drie voorspelbare sleepacties i.p.v. mikken op een tussenpositie.
    • Klap de te verslepen node eerst in (chevron) — dat houdt de boom kort genoeg om bron én doel tegelijk in beeld te hebben, wat nodig is omdat het boompaneel niet met het muiswiel scrollt.
    • Is de omhullende node na de verhuizing overbodig geworden (alleen nog padding/alignment): rechtsklik → Remove Widget haalt alleen die Container weg en tilt de kinderen een niveau omhoog — precies wat je wilt, en het scheelt het gevecht met de padding-velden (die vielen hier buiten het klikbare gebied, zie de clipping-notities elders).
    • "Node naar achteren verplaatsen" wordt één herhaalde sleep als je eerst alle siblings inklapt (bevestigd 2026-09-04, P1-41 op drie kaartcomponenten). Klap elke kind-rij van de doel-Column in (chevrons van onder naar boven, dan schuiven de nog te klikken chevrons niet), zet de te verplaatsen node erin (landt op positie 0) en sleep daarna telkens de onderste rij naar de Column-rij, net zo vaak als er siblings zijn. Bron- en doelcoördinaat blijven dan élke keer identiek — bij een Column met 5 kinderen zijn dat 5 exact dezelfde sleepacties.
    • Doe niet meer dan één sleep per tool-aanroep. Twee left_click_drags in één browser_batch (zelfs met 5s ertussen) lieten er structureel maar één landen, zonder foutmelding — de boom is nog aan het herrenderen. Verifieer elke sleep met een zoom op het boompaneel.
    • De Align-wrapper die een widget in een Stack had, hoort ná de verhuizing weg. Er is geen aparte Align-tree-node om te verwijderen: het is de sectie Alignment in het rechterpaneel van de verplaatste node zelf. Het ↺-icoontje rechts naast de kop "Alignment" wist X én Y in één klik, en dán verdwijnt de Align(...) ook echt uit de export (X/Y handmatig op -1/0 zetten laat de wrapper staan).

    Numerieke velden in het rechterpaneel (Padding, Border Radius, …): één left_click + ctrl+a + typen — triple_click focust ze niet. Bevestigd 2026-09-04, nadat triple_click + typen 3x achter elkaar niets deed (veld bleef de oude waarde tonen, geen foutmelding — hetzelfde "lijkt-gelukt"-patroon als elders). Mik op het getal zelf, niet op het vak eromheen: bij de padding-widget liggen de vier waarden dicht op elkaar en een paar pixels ernaast focust de buurwaarde (een klik bedoeld voor "bottom" landde op "left"). Gebruik zoom om de exacte pixelpositie van het getal te lezen, en verifieer daarna met een tweede zoom.

    Een Align/tag-blok uit een Stack naar een tekstkolom verplaatsen kost verticale ruimte — check een kaart met vaste hoogte daarna op een echt toestel. Bevestigd 2026-09-04 (P1-41): op PUitgaanSliderKaartComponent (carousel met vaste hoogte, 180 dp op telefoon) liep het verplaatste categorielabel meteen buiten de witte kaart — in een profile-build volledig stil: geen gele overflow-streep, niets in dart analyze, niets in logcat, alleen een half afgekapt label op de screenshot. Opgelost door de verticale padding van de kaart-Row van 20/20 naar 4/4 te zetten (32 dp gewonnen, ruim genoeg). Bij zo'n verplaatsing dus altijd: eerst kijken of de ouder een vaste hoogte oplegt, en daarna een screenshot op telefoonformaat (411 dp).

    Een ListView/GridView met pagination van widgettype wisselen — Replace weigert eerst. Rechtsklik → Replace geeft "Can't replace a widget that has pagination enabled. Go to the 'Backend Query' pane and remove pagination before proceeding." Werkend recept (bevestigd 2026-09-01/02 op HomeUitgaantabelKaartComponent, eerst uitgeprobeerd op een wegwerp-duplicaat):

    1. Backend Query → Edit → Enable Infinite Scroll uit → de page-variabele wordt rood/ongeldig → Remove → Confirm.
    2. Rechtsklik → Replace → kies het nieuwe type. De Backend Query, "Enable Pull to Refresh" én béide laadindicatoren (first page en next page) overleven de omzetting ongeschonden — alleen de page-variabele moet je terugzetten.
    3. Backend Query → Edit → Infinite Scroll weer aanSet Additional Variable → Parameter Name page → Value-icoontje → "Next Page Index (StaggeredView)" (het veld toont daarna "Pagination - Next Page Number", precies zoals het origineel) → Confirm.
    4. Kies StaggeredView, niet GridView, voor kaarten van ongelijke hoogte: een GridView dwingt een vaste childAspectRatio af, een StaggeredView genereert een PagedMasonryGridView dat variabele hoogtes aankan. Dat is ook het patroon dat op HorecagelegenhedenOverzicht al draait.

    Een component-parameter die wél gedeclareerd is maar nergens gelezen wordt, is een stille bug — en de builder waarschuwt er niet voor. Gevonden 2026-09-06 (P1-44): HomeUitgaantabelKaartComponent kreeg per Home-tab een eigen displayid mee, maar gebruikte 'm nergens; alle vijf de tabs toonden daardoor dezelfde lijst. Geen foutmelding, geen analyse-waarschuwing, en op een toestel volstrekt geloofwaardig (elke tab vult zich netjes met kaarten). Snelle audit in de export: voor elke component-parameter x grep -n "widget!\.x" <component>_widget.dart — komt hij alleen in de declaratie voor, dan wordt hij weggegooid.

    Een API Call een extra query-variabele geven (bv. om een hardcoded display_id overrulebaar te maken) — werkend recept, 2026-09-06. API Calls-paneel (⌘+K → "api" → "Tabs: API Calls") → call kiezen → tab Variables → "+ Add Variable" (naam + Type + Default Value) → Save → tab Query Parameters → "+ Add Query Parameter" → naam → Value Source "From Variable" → Select Variable → de zojuist gemaakte variabele → Save. Twee keer opslaan dus; de variabele moet bestaan vóór de query-parameter 'm kan kiezen.

    • Een dubbele query-parameter is geen probleem: de laatste wint. De URL van zo'n call houdt vaak zijn eigen ?display_id=services_2, en de nieuwe parameter komt daar áchter. Geverifieerd tegen Drupal: ?display_id=services_2&display_id=services_4 levert exact hetzelfde als alleen services_4. Je hoeft de URL dus niet te herschrijven — dat is ook precies hoe Uitgaanstabel het al deed.
    • Geef de nieuwe variabele als Default Value de waarde die nu hardcoded in de URL staat, dan verandert er niets voor aanroepers die 'm niet binden.

    Een variabele binden in een Backend Query mét Infinite Scroll: de page-binding overleeft dat gewoon. Recept: node selecteren → Backend Query (3e icoontje) → Edit (reken op twee klikken, de eerste opent 'm vaak niet) → sleep de onderrand van de dialoog omlaag (de "———" onderaan; anders valt de nieuwe rij eronder weg) → "+ Set Additional Variable" → Parameter Name kiezen → het icoontje naast Value → Set Variable → bron kiezen → Confirm. Klap daarna de page-rij open om te zien dat die nog op "Pagination - Next Page Number" staat vóór je Confirm drukt — dat is het enige echte risico van deze ingreep, en in dit geval bleef hij intact.

    "Generate Dynamic Children" accepteert GEEN custom function als waarde — dood spoor, niet opnieuw proberen. Bevestigd 2026-09-06: de waardekiezer toont in zijn bronnenlijst wél "Custom Functions" (zodra je op de functienaam zoekt), maar de sectie klapt leeg open — geen enkele functie is selecteerbaar voor het verwachte type List < Anything >. Ook niet na het return-type van de functie op JSON + Is List te zetten (dat levert List<dynamic>? op, wat precies zou moeten passen). Vier pogingen, met en zonder de bekende hover-truc; de sectie toggelt alleen tussen leeg-open en dicht. Wil je een lijst normaliseren vóór hij de loop in gaat: dat moet dus aan de databron gebeuren (de view/het endpoint), niet in de builder.

    Een Text/Wrap-loop over een JSON-veld dat soms een lijst en soms een string is, crasht STIL in een profile-build. Bevestigd 2026-09-06: getJsonField(item, r'$.veld').toList() op een String gooit NoSuchMethodError: Class 'String' has no instance method 'toList'. Op het toestel zie je geen rood scherm en geen overflow-streep — alleen een leeg vlak (of een grijs blok waar het widget hoorde), en dart analyze zegt niets, want getJsonField levert dynamic. Vind het terug in adb logcat/de run-log op de letterlijke tekst has no instance method — dat is het snelste bewijs. Reken er bij elk nieuw lijst-achtig veld op dat je de vorm eerst even met curl controleert vóór je erop bouwt.

    Breekpunten en het huispatroon voor responsieve waarden. De constanten staan in lib/flutter_flow/flutter_flow_util.dart: kBreakpointSmall 479, kBreakpointMedium 767, kBreakpointLarge 991 (logische px). Elke Responsive Value heeft dus vier takken. Het patroon dat in dit project overal is doorgevoerd:

    • kolommen (Cross Axis Count): 1 / 1 / 2 / 3 — horeca-overzicht, Home (alle 5 tabs via HomeUitgaantabelKaartComponent) en Favorieten.
    • Carousel viewportFraction: 0.75 / 0.75 / 0.5 / 0.35HomeUitgaanSliderComponent en PUitgaanSliderComponent (2026-09-03). Zonder dit werd de sliderkaart op een 1280 dp tablet 960 dp breed, met een brede grijze band eromheen. Merk op dat beide de eerste waarde herhalen: de twee smalste banden zijn allebei "telefoon" en verdienen zelden verschillende behandeling. Ter referentie: telefoon = 411 dp, tablet staand = 800 dp, tablet liggend = 1280 dp.

    Responsieve kolommen zetten: Cross Axis Count → icoontje naast het label → Source "Responsive Value". Dat bouwt zelf de vier breekpunt-takken (< kBreakpointSmall / < Medium / < Large / anders); vul de waarden in en klap "Return Type (Optional)" één keer open vóór Confirm. Kritieke valkuil: sleep de dialoog aan het handvat bovenaan omhoog vóórdat je "Responsive Value" kiest. De dialoog is te hoog voor het venster, waardoor de onderste twee waarden én de Confirm-knop wegvallen — en slepen ná het invullen sluit de dialoog en gooit je invoer weg (2x gebeurd 2026-09-02). Een lege tak levert return 0; op, wat bij een MasonryGridView een assertion gooit en de lijst leeg laat: altijd alle vier vullen.

    Dialoog "FlutterFlow Version Out of Date" = iemand anders heeft tegelijk in het project gewerkt. Verschijnt zodra je een edit probeert terwijl je browsersessie op een oudere projectversie staat; de melding vraagt om verversen en dat is ook de enige route (knop "Refresh", daarna ±60s herladen). Zie je hem: ga er niet van uit dat jouw laatste export nog de actuele staat weergeeft — draai er een verse. Bevestigd 2026-09-02, toen Bob en Claude onafhankelijk hetzelfde component omzetten.

    Een taxonomie-/categorie-id verifiëren zonder het te gokken: test het endpoint rechtstreeks. Bevestigd nuttig 2026-09-02 (een horcat was naar een niet-bestaande categorie gezet; de tab bleef leeg zonder foutmelding). De establishments-view heeft een townid nodig, anders geeft élke categorie []:

    curl -s -u bob:serhii "https://uitgaanskrant.com/nl/flutterdrup/views/flutterflowmobiel_establishments.json?horcat=<id>&townid=<tid>&display_id=services_1&page=0"
    

    Een geldige townid haal je op via plaatsen.json (provincies) → plaatsen.json?limit_levels=2&provincieid=<pid> (gemeenten) → plaatsen_bij_gemeente.json?gemeenteid=<gid> (plaatsen). Arnhem (25434) is een handige testplaats met content in vrijwel elke categorie. Vergelijk het aantal resultaten én de categorie-labels tussen kandidaat-id's; dat maakt in één keer hard welk id de bedoelde inhoud levert.

    Een nieuwe rij toevoegen aan een lijst met bestaande, vergelijkbare rijen (bv. nog een ListTile in een menu): dupliceer een bestaande rij i.p.v. Insert Widget te gebruiken. Rechtsklik de bestaande node → Duplicate (Ctrl+D) → de kopie verschijnt als sibling → pas titel- tekst en On Tap-actie aan op de kopie. Betrouwbaarder dan de bekende Insert-Widget-onbetrouwbaarheid (zie hieronder), en scheelt het handmatig opnieuw opbouwen van styling (padding, border radius, tileColor etc.). Bevestigd 2026-08-24 op Favorieten' Gebruiker-tab ("Account verwijderen"-knop toegevoegd naast de bestaande "Uitloggen"-knop).

    "Launch URL"-actie staat onder categorie "Share" in de Actions-zoeklijst, niet onder "Navigation" — logisch te missen. Zoek in de Action Flow Editor gewoon op "Launch URL", dat vindt 'm ongeacht categorie.

    Tooltip toevoegen aan een icon-only widget: rechtsklik → Wrap Widget (Ctrl+B) → 4e rij van de grid (kan geclipt lijken) → 1e icoon (spraakwolkje) = "Tooltip". Genereert een AlignedTooltip, geen losse styling nodig. Werkt niet op iconen embedded als suffixIcon van een TextFormField (bv. Login-pagina wis-/toon-wachtwoord-iconen — geen losse wrapbare tree-node).

    Een Row/Column met gegenereerde kinderen (List.generate(...)) van widget-type wisselen (bv. Row → Wrap tegen tag-overflow): gebruik rechtsklik → "Replace", NOOIT "Wrap Widget" gevolgd door "Remove Widget" op de oude node. Bevestigd 2026-08-12 (live pair-sessie, HomeUitgaantabelKaartComponent): de "herhaal-over-lijst"-configuratie (List.generate, incl. de parameter-binding zoals categorieTag: categorieitemItem) hangt als eigenschap ván de omringende Row/Column zelf, niet van het kind-template. "Wrap Widget" wrapt de Row in een nieuwe Wrap (dubbele nesting, lost niets op) en de daaropvolgende "Remove Widget" op de overbodige oude Row verwijdert de hele lijst-generatie mee — resultaat is een enkel statisch, ongebonden kind-widget i.p.v. een item per lijst-element (functionele regressie, geen crash/foutmelding die het verraadt). Herstel: Ctrl+Z (werkte binnen dezelfde tab/sessie, geen garantie na page-reload). Bevestigd werkende, veilige methode: rechtsklik direct op de Row → Replace → kies Wrap. Dit wisselt alleen het widget-type, List.generate + alle parameter-bindingen blijven intact (bevestigd via verse export op meerdere componenten). Zelfde controlestap als altijd: ná de wijziging verifiëren dat List.generate(...) nog in de export staat, niet alleen dat de tree er goed uitziet.

    Widget toevoegen: gebruik de kleine inline "Insert Widget", niet de grote centrale "Insert"-modal. Rechtsklik op een Widget Tree-rij → "Insert Widget" (of het kleine "+"-icoon naast een tree-node) opent een compacte dropdown die betrouwbaar werkt: klik op een widget-kaart voegt 'm meteen toe. De grote centrale Insert-modal (bv. via de "+"-knop bovenaan het linkerpaneel) opent wél, en widget-kaarten lijken klikbaar/highlighten bij hover, maar een klik registreert niet — geen insertie, dialoog blijft open, geen foutmelding. Bevestigd 2026-08-05 op EventCurrent (Button toevoegen na EvenementHorecagelegenheid): pas de inline-variant via rechtsklik werkte. Sluit aan bij een eerdere observatie (2026-08-04, op HorecagelegenheidCurrent): een widget toevoegen in een AppBar-Row faalde herhaaldelijk stil via beide insert-varianten, terwijl exact dezelfde inline-picker op een gewone body-Column wél meteen werkte — mogelijk is een AppBar-Row specifiek een moeilijkere insertie-plek, sowieso eerst de inline-variant op een Column proberen vóór de grote modal.

    • Update 2026-09-04 (P2-6-test): rechtsklik → "Insert Widget" opent hier tegenwoordig de grote centrale modal, en die werkt soms wél. Op een kale TabBar Page > Column lukte het in één keer: rechtsklik op de tree-rij → "Insert Widget" → meteen typen (het zoekveld is al gefocust, klik er niet eerst in — dat brak het juist) → kaartje aanklikken. Het widget landt achteraan in de Column, niet vooraan (anders dan bij slepen in de tree, dat altijd op positie 0 dropt). Op een Column binnen een ConditionalBuilderIf-tak lukte precies dezelfde reeks 3x níet: het kaartje aanklikken doet niets, de dialoog blijft open, geen foutmelding — en ook niets kapot. Vermoeden, niet bewezen: insertie in een ConditionalBuilder-tak is het probleem.
    • Update 2026-08-07: de compacte inline-dropdown is niet betrouwbaar per se buiten AppBar-Row zoals hierboven gesuggereerd — op HorecagelegenhedenOverzicht (een gewone Column, TabBar-node) opende rechtsklik → "Insert Widget" herhaaldelijk wél de grote, kapotte modal (klikken/dubbelklikken/zoeken op widgetnaam, niets registreerde — 5+ pogingen). Blijkbaar niet 100% voorspelbaar per context. Werkende omweg wanneer je alleen decoratie/styling nodig hebt (geen echt nieuw kind-widget), bevestigd op ditzelfde component: i.p.v. een nieuwe widget te inserten, de bestaande node (bv. TabBar, of een Column met tags) Wrap Widget (Ctrl+B) → Container — dat dialoogvenster werkt wel betrouwbaar (bevestigd meerdere keren deze sessie) — en geef die nieuwe Container dan een BoxShadow/Fill Color/Border Radius via de eigenschappen-zoekbalk. Voor een schaduw onder een widget (bv. TabBar-afscheiding): Box ShadowShadow Color (hex-veld, bv. 33000000) → apart zoeken op "offset y" (aparte property, niet samen met "offset" x/y in één filter) en "blur". Voor een simpele achtergrond-scrim (bv. tegen storende achtergrond-afbeelding): alleen Fill Color op de wrap-Container volstaat al, ook zonder eigen padding als de wrap toevallig al een bestaande Padding-widget omvat. Bij een écht nieuw structureel widget (geen bestaande node om te wrappen) blijft de Insert-modal het enige pad — dan na 1-2 pogingen stoppen en aan Bob overdragen, zoals altijd.

    Vertalingen invullen: het Settings → Languages-paneel SCROLLT NIET voor Claude — gebruik het globe-icoontje op het widget zelf. Bevestigd 2026-09-08 (P1-17, 22 vertalingen doorgezet). Het Languages-paneel (App Settings → Project Setup → Languages; de URL ?tab=appSettings&appSettingsTab=languages werkt alleen via de linkerrail, direct navigeren redirect stil naar de laatst geopende pagina) toont per pagina/component een sectie met Dutch/English naast elkaar — prettig werken, maar de sectielijst is niet te scrollen: muiswiel op drie verschillende posities, Page_Down, Tab-navigatie vanuit een tekstveld én left_click_drag doen géén van alle iets. Alleen de bovenste ~14 secties zijn bereikbaar, en dat zijn precies de pagina's; álle componenten (drawerComponent incl. het hele hoofdmenu) zitten dieper. Ruimte winnen door het "Languages"-blok bovenaan in te klappen (chevron rechts) levert 3-4 extra rijen op, meer niet. In Bob's eigen browser werkt dit paneel waarschijnlijk gewoon — voor hem blijft het de snelste route.

    Werkend alternatief, per widget (bevestigd 22x achter elkaar): selecteer het widget → in het rechterpaneel staat naast het label "Text" (bij een Text-widget onder "Text Properties", bij een ListTile onder "Title", bij een knop onder "Button Text") een klein globe-icoontje → klik dat → het hele rechterpaneel wordt vervangen door "Setting Translations" met de widgetnaam, een Dutch- en een English-veld, en onderaan Back + "Google Translate" (die laatste niet gebruiken, betaald). Vul English, klik Back. Drie dingen die je moet weten, anders werkt het stil niet:

    1. Het typen komt pas in een VOLGENDE tool-call aan. De eerste triple_click op het English-veld direct na het openen van het paneel wordt genegeerd — ook met 10s wachttijd ertussen, en ook als je in dezelfde batch twee keer triple-klikt. Vaste werkwijze: één call opent de globe + doet een eerste triple_click + zoom (waarmee je meteen ziet wélke Nederlandse waarde je beet hebt), de vólgende call doet triple_click + type + zoom ter controle.
    2. Klik de globe niet te snel na het selecteren van de tree-node. Doe je dat wel, dan toont het translations-paneel de kop van het nieuwe widget maar de tekstwaarde van het vórige — typ je dan, dan bewerk je de verkeerde sleutel. Wacht ~8s en verifieer eerst met een zoom op de Title/Text-sectie dat het properties-paneel al de nieuwe tekst toont.
    3. Staat er al een Engelse vertaling, dan is dat vaak géén fout maar eerder werk — controleer met de export vóór je overschrijft.

    Tree-zoekveld: de eerste klik op een gefilterd resultaat landt niet, de tweede wel. Bevestigd 2026-09-08, consistent: na het typen in "Search for widget..." houdt dat zoekveld de focus, waardoor een klik op een boomrij wel de rij markeert (bruinig, hover-achtig) maar het rechterpaneel niet ververst. Klik gewoon twee keer op dezelfde coördinaat. Handig bijkomend gegeven: bij een gefilterde boom staan de padnodes altijd bovenaan, dus de doelnode zit op een voorspelbare y-positie — filteren op een gedeelde prefix (bv. ListTile) zet alle gelijksoortige widgets in één voorspelbare kolom, wat het herhaalwerk sterk versnelt. Let op dat de boom ondertussen kan verschuiven: maak elke paar items een verse zoom van het boompaneel in plaats van onthouden coördinaten te hergebruiken.

    De builder kan tijdens dit werk spontaan volledig grijs worden. Gebeurde 2026-09-08 één keer midden in een globe-klik: het hele canvas + beide panelen werden één egaal grijs vlak, Escape hielp niet. Een navigate naar dezelfde URL herstelde alles zonder verlies — de al doorgezette wijzigingen stonden gewoon in de export. Niet in je eigen laatste actie gaan zoeken; gewoon herladen en verdergaan.

    Een tab toevoegen aan een bestaande TabBar: via de "Active Tab"- dropdown, niet via Insert Widget. Gevonden 2026-08-15 (P1-7, Favorieten kreeg een 4e tab "Gebruiker"). Selecteer de TabBar-node zelf (niet een losse Tab) → rechterpaneel toont bovenaan "Active Tab" (een dropdown die de huidige preview-tab kiest, met de bestaande tabnamen als opties) → klik erop → onderaan de openklappende lijst staat "+ Add Tab". Dit voegt in één klik zowel een nieuwe Tab-label (in de TabBar's tabs:-lijst) als een bijpassende lege TabBarView-pagina toe (in de tree als een nieuwe Tab+TabBar Page- paar onderaan) — geen losse Insert Widget-stappen nodig, en geen TabController-length-gedoe (FlutterFlow past die automatisch aan). Nieuwe tab heet standaard "Tab N"; hernoem via de Tab-node se­lecteren → rechterpaneel "Label → Text"-veld. Let op: het rechterpaneel kan na het typen even de oude waarde blijven tonen (react-render-lag) — het canvas zelf is al bijgewerkt en is de betrouwbaardere check; een her-select van de node bevestigt de opgeslagen waarde.

    • In de widget tree zijn Tab-nodes te herkennen aan een "+"-icoon (het icoon-lettertype voor het Tab-widgettype), niet te verwarren met een "voeg widget toe"-knop — het is gewoon de bestaande tab-label-node, aanklikbaar als elke andere tree-rij.

    Per-tab "On Tap"-logica van een TabBar (bv. data ophalen bij het wisselen van tab) leeft op de individuele Tab-node, niet op de TabBar zelf. Gevonden 2026-08-17: de TabBar-node heeft zelf ook een Actions-tab met een "Action Flow Editor", maar díe biedt alleen generieke gestures (On Double Tap/On Long Press/On Tab Change) — leeg tenzij je het zelf instelt. De bestaande, al werkende per-tab-fetch zat in werkelijkheid op elke losse Tab-node (bv. TabPersAgenda) se­ lecteren → Actions-tab → "On Tap: N actions". FlutterFlow bundelt deze losse per-tab-acties in de export tot één onTap: (i) async { [...][i]() } op de gegenereerde TabBar. Praktisch gevolg: zo'n per-tab-actie vuurt nooit voor de al-actieve standaardtab bij page-load (onTap reageert alleen op een echte tik) — heb je ook data nodig zodra de pagina opent, voeg dan apart een "On Page Load"-trigger toe op de Scaffold (zelfde als bij een gewone pagina) met dezelfde actieketen.

    • Snelste manier om die actieketen te dupliceren: rechtsklik op de eerste actie van de bestaande per-tab-flow → "Copy Action Chain" → ga naar de nieuwe On-Page-Load-flow → "Paste Action(s)". Werkt betrouwbaar, maar let op deze valkuil: als de gekopieerde actieketen een Custom Action met een "Action Output Variable Name" bevat (bv. drupalRequest's resultaat), plakt FlutterFlow die variabele met exact dezelfde naam als een nieuw, apart model-veld — dat geeft in de export een echte Dart-compile-error ("X is already declared in this scope", 2x gedeclareerd in ..._model.dart). Build faalt dan pas bij de eerstvolgende flutter run/export-code, niet zichtbaar in de builder-UI zelf (geen foutmelding daar). Fix meteen ná het plakken: open de geplakte Custom-Action-node → scroll (of klap "method"/eerste argument dicht om ruimte te maken, zie de rechterpaneel-clipping- notitie hieronder) naar "Action Output Variable Name" → geef 'm een unieke naam → ga naar de vólgende actie in dezelfde geplakte keten die deze variabele gebruikt (bv. een "Update Page State") → klik het Value-veld → potlood-icoon → Source "Action Outputs" → kies de net-hernoemde variabele opnieuw (de oude referentie blijft anders naar de verwijderde/foutieve naam wijzen). Altijd verifiëren met een verse export + flutter analyze vóór je opnieuw deployt — dit patroon is 2x in dezelfde sessie misgegaan voordat de fix duidelijk was.

    Meerdere acties na elkaar op één trigger (bv. eerst App State opruimen, dán navigeren) instellen via de "Action Flow Editor", niet door in het compacte Actions-paneel te blijven scrollen. Na de eerste actie (Actions-tab → Add Action → ...) toont het paneel geen zichtbare "volgende actie toevoegen"-knop meer onderaan de eerste actie's eigen instellingen. Klik in plaats daarvan op "Edit" naast "Action Flow Editor" (bovenaan de Actions-tab) — dat opent een los canvas met de acties als verbonden blokken en een "+"-connector tussen/onder elk blok. Klik de "+" onder het laatste blok → "Add Action" (naast Add Conditional/Loop/Parallel/Paste) → kies de volgende actie zoals gewoonlijk. "Close" rechtsboven sluit het canvas weer, de acties blijven gewoon staan.

    • "Update App State" met meerdere velden tegelijk wissen (bv. een logout-knop): per veld eigen "Select Update Type"-dropdown, kies "Clear Value" (niet "Set Value"). Add Action → State Management → Update App State → "+ Add Field" (zoekbalk toont alle App State-velden, ook niet-zichtbare zoals userMail — gewoon de veldnaam typen als hij niet meteen in de eerste ~6 resultaten staat) → per toegevoegd veld een eigen "Select Update Type" instellen op "Clear Value" — reset het veld naar zijn lege/default-waarde (''/0), genereert FFAppState().deleteX(); FFAppState().x = ''/0; in de export. Voor "Navigate To" ná zo'n reset: zet "Allow Back Navigation" uit als de gebruiker niet terug moet kunnen naar de net-verlaten (nu uitgelogde) pagina — genereert context.goNamed(...) i.p.v. context.pushNamed(...), dus geen back-stack-entry.

    Set-Variable/parameter-waarde koppelen aan App State: klein icoontje naast het "Value"-label, niet de tekstinvoer zelf. Bij een actie-parameter (bv. Navigate To met page-parameters) toont het "Value"-veld standaard een tekstinvoer (voor een letterlijke waarde). Om aan een variabele (App State, Page Parameter, …) te binden: klik op het kleine icoontje vlak vóór/naast het woord "Value" (niet in het invoerveld zelf) — dat opent een "Set Variable"-dialoog met een zoekbalk en een "Source"-lijst (Page Parameters/App State/Global Properties/…) om uit te klappen en te doorzoeken. Dit icoontje is makkelijk te missen/verkeerd te klikken (kleiner dan de rest van het paneel) — bij twijfel zoom gebruiken op de regio rond "Value" om de exacte pixelpositie te bepalen vóór je klikt.

    • Binden aan een veld van een lijst-item (bv. een ListView/GridView itemBuilder-variabele als "evenementen item"), 2026-08-07 bevestigd: het item zelf selecteren in de Set Variable-dialoog is niet genoeg — je krijgt dan het hele JSON-object, niet een veld ervan. Klik het item, dan "Available Options" (dropdown, staat eerst op "No Further Changes") → "JSON Path" → typ het pad met een voorloop-$, bv. $.nid of $.horecagelegenheidid (exacte veldnamen uit de API-respons, zie lib/backend/api_requests/api_calls.dart). Werkt identiek aan de al bestaande getJsonField(item, r'''$.veld''')-code elders in dit project — dit is precies hetzelfde patroon, alleen via de builder-UI ingesteld i.p.v. handmatig in code.
    • Direct binden aan een bestaande Component Parameter (niet JSON Path) is minder betrouwbaar dan het lijkt — altijd verifiëren via View Code. Op een non-JSON-Path-binding (klik direct op een Component Parameter-naam in de Set Variable-lijst, geen "Available Options" nodig omdat het al de juiste String is) leek de builder-UI 2026-08-07 een keer de waarde correct te tonen (Value-veld toonde de parameternaam oranje) maar genereerde de export toch serializeParam('', ParamType.String) — een lege string i.p.v. de parameter-referentie. Reproduceerbaar herstel: Value-icoontje opnieuw aanklikken, dialoog opnieuw openen, dezelfde parameter opnieuw selecteren — de tweede keer sloeg het wél correct op. Vertrouw dus nooit alleen op het rechterpaneel na zo'n binding; controleer altijd even via View Code (zie hieronder) dat de gegenereerde code echt naar de variabele verwijst en niet naar een lege/letterlijke waarde.

    Zelfde "leek opgeslagen, was het niet"-patroon ook bevestigd op een kaal letterlijk Action Flow Editor-tekstveld (niet alleen bij variabele-bindingen). Bevestigd 2026-08-17 (login_widget.dart, "Inloggen"-knop → Action Flow Editor → "Show Snack Bar"-actie → "Snack Bar Message" → Value): eerste keer typen + wegklikken toonde de tekst gewoon in het paneel, maar een verse export gaf toch content: Text('', ...) — lege string. Tweede poging (triple-click op het veld → opnieuw typen → Tab om te bevestigen → her-selecteren van de actie ter controle dat de waarde bleef staan) sloeg wél correct op, bevestigd via nieuwe export. Vuistregel dus verbreed: bij élk Action-tekstveld (niet alleen Component-Parameter-bindingen) een losse verificatie-export doen vóór je verdergaat — het rechterpaneel alleen is niet genoeg bewijs dat een waarde echt is doorgezet.

    API Calls-paneel: "Delete" op een losse call kan ook niet doorzetten naar de export — niet alleen een widget-tree/canvas-probleem. Bevestigd 2026-08-14 (FavorietenAgendaTESTKANWEGCall, P2-7): Delete- knop → bevestigingsdialoog ("Delete this API Call from the project?") → Delete — de dialoog sluit zichtbaar netjes (geen freeze), maar de call staat na een page-reload gewoon weer terug in de lijst, 2x gereproduceerd, ook bevestigd via een losse verse flutterflow export-code (klasse nog gewoon aanwezig). Dit paneel is een platte lijst, geen canvas-coördinaten — dus dit is geen instantie van de bekende widget-tree-klikonbetrouwbaarheid, eerder een breder "ziet er opgeslagen uit in de UI, bereikt de export niet"-patroon (zie ook P1-13/P0-3 punt 1 in TASKS.md) dat blijkbaar ook dit paneel kan raken. Zelfde vuistregel: na 1-2 pogingen niet blijven proberen, aan Bob overdragen (zijn eigen browser/sessie kan een andere uitkomst geven, nog niet getest).

    Het Path-veld van een Image accepteert alleen een variabele die als type "Image Path" gedeclareerd is — een gewone String wordt gedimd getoond en is niet selecteerbaar. Bevestigd 2026-09-01 (Bob): FlutterFlow heeft voor App/Page State een apart Image Path-datatype, en de "Set from Variable"-dialoog (kop: "Type: Image Path") matcht daar strikt op. Een Page State-veld van type String met een geldige URL erin is dus onbruikbaar voor dit veld totdat je het type van de variabele wijzigt naar Image Path. Onder water verandert er niets — de export houdt gewoon String? _createLogoUrl — het is puur een type-label in de builder. Verwarrende bijkomstigheid die je op een dwaalspoor zet: lijst-variabelen worden in diezelfde dialoog wél als selecteerbaar getoond, óók een List<Json>-custom-function, wat de indruk wekt dat het veld een lijst wil. Dat is het niet. Zie je alleen lijsten oplichten bij een Path/typed veld: wijzig het type van de bronvariabele, ga niet zoeken naar een lijst-omweg of een custom-function-truc. Let op de gegenereerde !: een gebonden Path levert Image.network(_model.createLogoUrl!, ...) op — zonder Visibility- conditie ("Is Set and Not Empty") crasht de pagina met Null check operator used on a null value zolang de variabele nog leeg is. Zelfde patroon als bij een verplichte page-parameter in een custom action.

    Een FlutterFlow API Call KAN een Drupal-sessiecookie meesturen — de aanname dat daarvoor drupalRequest nodig was, is onjuist. Bevestigd 2026-08-31 (live op toestel). Het ging altijd mis op de syntax: een headerwaarde {{session_name}}={{sessid}} is Postman-notatie, en ApiManager doet géén {{ }}-substitutie — die tekst wordt letterlijk verstuurd, Drupal ziet een anonieme bezoeker en geeft geen array terug, waarna getJsonField(...) as List?)! crasht. FlutterFlow wil rechte haken: [session_name]=[sessid], wat in de export echte interpolatie oplevert ('Cookie': '${sessionName}=${sessid}'). Er zijn twee kanten die allebei moeten kloppen — check ze samen in de export:

    1. de call zelf: 'Cookie': '${sessionName}=${sessid}' (niet {{…}});
    2. de aanroepende Backend Query: XCall.call(sessionName: FFAppState().userSessionname, sessid: FFAppState().userSessionid) — ontbreken die argumenten, dan vallen ze terug op '' en gaat er Cookie: = mee, ook al is de header perfect. Vuistregel wanneer wat: lezen (GET, platte query-params) → gewone API Call, dan krijg je FutureBuilder/laadstatus/caching gratis. Schrijven met een niet-platte body (geneste structs, arrays), eigen foutafhandeling of logica vóór/ná het verzoek → custom action. Dát is de echte reden dat stadsactiviteitCreate/bestandUpload bestaan.

    Een widget die door layout-overflow buiten beeld valt is in een profile-build volledig STIL — geen gele overflow-streep, niets in logcat, niets in dart analyze. Diagnose: get_ui_tree, niet een screenshot. Bevestigd 2026-09-03 (stadsactiviteitAanmaken): drie cascade-dropdowns stonden naast elkaar in een Row, elk width: 200. Op een telefoon van 411dp is 3 × 200 = 600, dus de derde viel volledig buiten het scherm — en daarmee was het formulier niet in te dienen, want de verzendknop hing aan een Is-Set-guard op die derde waarde. Op een screenshot zie je alleen "er staat hier niets", wat net zo goed een verborgen widget of ontbrekende data kan zijn. get_ui_tree maakt het hard: het weggevallen widget staat niet in de accessibility-tree, terwijl een verborgen-maar-aanwezig widget er wél in staat. Reken bij een Row met vaste breedtes dus altijd even uit of N × breedte binnen de smalste doeldevice-breedte past (telefoon-AVD = 411dp).

    Layoutkeuze voor een afhankelijke keten van dropdowns (provincie → gemeente → plaats): een gewone Column, niet Wrap en al helemaal niet StaggeredView. Overwogen en verworpen 2026-09-03:

    • StaggeredView bestaat om een lijst met één item-template te herhalen (Backend Query + Generate Dynamic Children). Bij losse widgets met elk hun eigen query en eigen Visibility-conditie is er niets om over te herhalen, en je sleept het hele bounded-height-probleem mee (Shrink Wrap uit, Expanded, niet-scrollende ouder — zie het Home-recept elders in dit bestand).
    • Wrap laat kinderen naar de volgende regel vallen als ze niet passen, dus je krijgt per schermbreedte een andere indeling. En double.infinity is in een horizontale Wrap net zo ongeldig als in een Row, dus je blijft aan vaste breedtes vastzitten.
    • Column klopt inhoudelijk (opeenvolgende keuzes), accepteert infinity gewoon, en sluit aan bij de rest van een formulier. Praktisch: zitten de dropdowns al in de Column van een groep-Container, dan is rechtsklik op de Row"Remove Widget" genoeg — die haalt alleen de Row weg en schuift de kinderen een niveau omhoog. infinity weigert FlutterFlow met "Invalid Action" zolang ze nog Row-kinderen zijn; dus eerst uit de Row, dán de breedte.

    FlutterFlow's default fillColor op een TextField/DropDown is secondaryBackground — precies de kleur van een witte kaart, dus binnen zo'n kaart zijn je invoervelden ONZICHTBAAR. Bevestigd 2026-09-03 op stadsactiviteitAanmaken: 18 widgets (12 tekstvelden + 6 dropdowns) genereerden filled: true, fillColor: ...secondaryBackground mét enabledBorder/focusedBorder op Color(0x00000000) (ook een default). Resultaat op het toestel: alleen zwevende grijze labeltekst, geen zichtbaar invulvak — het leest als losse tekst in plaats van een formulier. Fix die hier gekozen is: fillColorprimaryBackground (#F2F2F2), randen transparant laten. Eén waarde per widget, geeft een zacht grijs vak op de witte kaart en sluit aan bij hoe uitgaanskrant.com zijn invoervakken vult. Alternatief is een rand op alternate (#EEEEEE), maar dat is 2 velden per widget en die kleur is al de kaartrand, dus het wordt vlak. Controleer dit standaard bij elke nieuwe formulierpagina — het is een default, dus het gebeurt vanzelf opnieuw (o.a. te verwachten op uitgaansevenementAanmaken). ⚠️ Verwarring vermijden: een Text-widget (sectiekop) heeft géén Fill Color, alleen Text Color — die twee worden makkelijk verwisseld. Zit je in een paneel met secties als "Enabled Border"/"Focused Border", dan heb je het juiste widget te pakken.

    In een profile-build rendert een crashende subtree als een neutraal GRIJS BLOK, niet als het rode foutscherm — en alles eronder in dezelfde Column verdwijnt mee. Bevestigd 2026-08-31: één kapotte dropdown bovenin maakte vier widgets eronder (incl. de verzendknop) onzichtbaar, wat de indruk gaf dat ze nooit gebouwd waren. Zie je zo'n grijs vlak: adb logcat/de run-log heeft de echte stack trace (let op: slechts één volledige dump per proces, zie elders in dit bestand). Voor dit soort speurwerk is een debug-build (ff-run-fvm.sh) juist prettiger dan profile — daar krijg je het rode scherm mét bestandsnaam en regelnummer.

    Een datumpicker op een readOnly TextField: de actie moet op een omhullende Container's On Tap, niet op de TextField zelf. Een TextField biedt in FlutterFlow alleen On Submit / On Change / On Focus Change — géén On Tap. Op "On Submit" (onFieldSubmitted) gaat de picker nooit af, want een readOnly veld kan niet ingediend worden. Werkend recept: Wrap Widget → Container, actieketen daarheen verplaatsen ("Copy Action Chain" → "Paste Action(s)"), oude On Submit-keten verwijderen, en op het TextField Enabled uit — een readOnly veld vangt de tap namelijk zélf op en geeft 'm niet door aan zijn ouder; pas een disabled veld laat 'm door. readOnly mag aan blijven. Let bij het plakken op de bekende hernummer-valkuil: de laatste actie las datePicked0 terwijl de keten datePicked1 vulde → harde compile-error.

    Een conditie op een JSON-veld dat geen echte bool is, genereert if (getJsonField(...)) zonder null-check — compileert wél, crasht bij runtime. Bevestigd 2026-08-31: een Action-Flow-conditie op $.nid gaf een bool-cast op een String/null (dart analyze ziet dit niet, want getJsonField levert dynamic). Kies een sleutel die écht een bool is (bv. $.success), of bouw een Single Condition met een expliciete operator. Bij een Visibility-conditie genereert "Is Set" wél netjes != null — het verschil zit in waar de conditie staat.

    Een widget kan zijn EIGEN Backend-Query-response niet gebruiken in zijn eigen Visibility-conditie — wel in zijn Options. De Set-from-Variable-dialoog toont die response daar simpelweg niet als bron. Oplossing: Wrap Widget → Column, de Backend Query naar die wrapper verplaatsen (en van het kind verwijderen), daarna is de response in scope voor zowel de Options als de Visibility van het kind. Bevestigd 2026-08-31 op de stadsrechten-shortcut.

    API-call headers: check op hardcoded literals i.p.v. [varname] templates. Werkt een call via curl wél maar vanuit de app/Response & Test-panel niet: check het Headers-tabblad — een handmatig ingeplakte testwaarde (bv. Cookie-header) kan per ongeluk blijven staan i.p.v. [sessionname]=[sessionid]. Check ook het per-variabele "Include"-vinkje in Response & Test — staat die uit, dan wordt de letterlijke [varname]-tekst meegestuurd i.p.v. de testwaarde.

    Custom Code-editor (Custom Functions/Widgets/Actions): tekst-editor, betrouwbaar te bewerken — maar wijziging verschijnt pas in een export ná een expliciete Ctrl+S. In tegenstelling tot widget-canvas/ rechterpaneel (coördinaat-onbetrouwbaar, zie hieronder) is dit een echte Monaco-editor: getypte code (incl. auto-closing brackets/quotes) komt gewoon correct aan, geen speciale precautions nodig. Kritieke valkuil, 2026-08-09 bevestigd: de header toont "Synced" al direct na typen, maar dat weerspiegelt niet dat de wijziging al naar de export-pipeline is doorgezet — een flutterflow export-code direct daarna (ook na een volledige herstart van ff-run-fvm.sh, dus geen lokale cache-issue) gaf 2x achter elkaar nog de oude bestandsinhoud terug. Pas ná een expliciete Ctrl+S in de editor (zichtbaar: kort "Syncing…" i.p.v. "Synced", + auto-format herschrijft de zojuist getypte regels) nam een verse export de wijziging over. Vuistregel: na elke Custom Code-wijziging altijd Ctrl+S drukken vóórdat je ff-run-fvm.sh/export-code draait, en bij twijfel eerst verifiëren met een losse flutterflow export-code --project <id> --dest /tmp/check --token "$(cat ~/.ff_token)" --no-parent-folder --as-debug + grep op de verwachte regel, i.p.v. blind op de "Synced"-badge te vertrouwen.

    • Bereikbaar via het ⌘+K-commandpalet (zoek op de custom-action-naam, bv. "drupalLogin") — betrouwbaarder dan door de linkerzijbalk klikken (die leed in deze sessie aan hetzelfde klik-coördinaat- probleem als de widget-tree, zie hieronder). Let op: klik het zoekresultaat aan met de muis, druk geen Enter — een nog openstaand rechtsklik-contextmenu elders op de pagina kan de Enter-toets onderscheppen (2026-08-09 1x per ongeluk een widget gedupliceerd op deze manier; met Ctrl+Z + Delete op de duplicaat hersteld, geen blijvende schade). Sluit dus eerst met Escape elk open contextmenu vóór je ⌘+K gebruikt.

    Widget Tree/canvas: klik-coördinaten zijn NIET betrouwbaar 1-op-1 met wat een screenshot toont, en dit is geen vast te rekenen offset. Los van de al bekende rechterpaneel-breedte-clipping (hierboven) bleek 2026-08-09: een klik op een zichtbare tree-rij selecteerde herhaaldelijk een héél andere rij (soms 2, soms 3 rijen verderop, niet consistent in pixels of in aantal rijen). Werkende aanpak: na een klik nooit blind vertrouwen — verifieer met Home + Shift+End (selecteert de hele regel/rij zonder iets te wijzigen) of een read-only zoom op het rechterpaneel, en pas de klik-y-coördinaat handmatig aan op basis van wat je net echt selecteerde, vóór je verdergaat. Reload van de pagina (navigate naar dezelfde URL) hielp de eerstvolgende klik weer correct te laten landen, maar het patroon kwam later opnieuw terug — geen structurele fix gevonden, alleen de per-klik-verificatie hierboven. double_click op een widget (zowel canvas als tree) selecteerde herhaaldelijk de root-pagina i.p.v. in te zoomen — vermijd double-click voor selectie, gebruik single-click + verificatie.

    • Werkende widget-verwijder-recept (bevestigd 2026-08-09, 5x succesvol op Login-knoppen): de zwevende toolbar boven een geselecteerd canvas-widget (prullenbak-icoon) lijdt aan hetzelfde coördinaat-probleem en klikt daardoor vaak mis (geen zichtbare fout, gewoon geen effect). Betrouwbaarder: rechtsklik op de Widget Tree-rij (niet het canvas) → tekst-menu-item "Remove Widget" (met woorden, niet het icoon) aanklikken. Werkte 5x op rij zonder falen zodra eenmaal de juiste rij geselecteerd was (zie hieronder voor selectie). Een simpele Delete-toetsaanslag wordt door de Bash/computer-permissie-classifier geblokkeerd als destructieve actie — niet bruikbaar, gebruik het menu-item.
    • Selectie-kalibratietruc: bij twijfel over de klik-offset, klik eerst op een duidelijk identificeerbare rij (bv. de eerste) en vergelijk in het rechterpaneel welke rij echt geselecteerd werd. Het verschil (in pixels) tussen bedoelde en werkelijke rij is op dat moment constant voor de rest van diezelfde paneel-render — pas die vaste correctie toe op volgende kliks i.p.v. te blijven gokken. Geldt zowel voor links- als rechtsklik.
    • Tekst typen in het rechterpaneel (bv. Hint Text/vertaalvelden) is op Claude's viewport structureel niet mogelijk gebleken (2026-08-09, login-hintText-veld). Klik, triple-click, Ctrl+A+typen, losse key-events, zelfs opzettelijk buiten-viewport-coördinaten (in de aanname dat click-ruimte niet 1-op-1 met screenshot-ruimte is) — niets kwam aan (waarde bleef letterlijk ongewijzigd). Dit is erger dan de bekende checkbox-clipping: niet alleen "net buiten beeld", maar functioneel onbereikbaar. Bob's tip (2026-08-09): voor een Text-widget zit naast het "Text"-label een klein bolletje/ globe-icoontje dat de vertaling direct opent — sneller dan via App Settings → Languages, en waarschijnlijk ook betrouwbaarder bereikbaar in Bob's eigen (niet-geclipte) browser. Nog niet bevestigd of dat icoontje voor Claude wél bereikbaar is (waarschijnlijk ook geclipt, niet meer getest deze sessie) — bij een volgende soortgelijke taak eerst dat proberen vóór je het opgeeft.
    • Visibility → "Conditional"-toggle (rechterpaneel, boven op elk widget) is voor Claude structureel niet betrouwbaar te klikken — apart bevestigd patroon, niet hetzelfde als de checkbox-clipping hierboven. Bevestigd 2026-08-14 op HorecagelegenheidCurrent (Text-adres, zie TASKS.md P0-8): 7+ pogingen (directe klik op de toggle-track op meerdere x/y-posities, twee keer na elkaar toggelen, left_click_drag over de track) gaven geen zichtbare statusverandering, en meerdere keren sprong de widget-selectie terug naar de pagina-root (rechterpaneel toonde dan opeens Scaffold/Edit Drawer-properties). De omweg via rechtsklik → Wrap Widget (Ctrl+B) opende het contextmenu zelf wél zichtbaar correct, maar de vervolgklik op een menu-item viel alsnog ten prooi aan de bekende tree-rij-offset-instabiliteit (offset was dit keer niet constant: +79px op het ene moment, +64px een moment later — dus ook de kalibratietruc hielp hier niet). Vuistregel: bij een taak die puur de Visibility-Conditional-toggle nodig heeft (aan/uit, geen vervang-logica), niet blijven proberen na 1-2 pogingen — teruggeven aan Bob met de exacte veldnamen/JSON-paths, dat kost hem seconden per veld.
    • Testen met een niet-Nederlandse systeemlocale: gebruik adb shell cmd locale set-app-locales <package> --locales nl-NL (per-app override, Android 13+/API 33+) i.p.v. het systeembrede settings put system system_locales + am broadcast -a android.intent.action.LOCALE_CHANGED — dat laatste faalt zonder root (Neither user ... nor current process has android.permission.CHANGE_CONFIGURATION) en werkte zelfs als root niet betrouwbaar zonder herstart. De per-app cmd locale-route werkte in één keer, direct zichtbaar na een am force-stop + relaunch, geen reboot nodig — dit is de snellere/betrouwbaardere manier om P1-17-achtige locale-afhankelijke bugs te reproduceren/verifiëren.

    Actions-paneel: bestaat wél, gevonden op 2026-08-14 (ListTile, niet op een Button geprobeerd, vermoedelijk hetzelfde) — eerdere notitie hieronder ("niet gevonden ondanks uitgebreid zoeken") was onvolledig zoeken, niet een echte afwezigheid. Selecteer het widget → kijk naar de kleine iconenrij direct onder de widgetnaam bovenin het rechterpaneel (naast de paint-brush/Styling-icoon): het 2e icoontje (een vertakkings-/node-icoon) opent de Actions-tab (kop "Define Action", een "On Tap"-trigger-dropdown + "+ Add Action"). Daar: Add Action → Navigation → Navigate To → doelpagina kiezen → Parameters → klik expliciet op "Pass" (niet op "+ Define" — dat opent per ongeluk de doelpagina's eigen parameterschema-editor, geen waarde-invoer voor déze actie) → per parameter verschijnt dan een rij met een Value-veld → normale Set from Variable-flow (zie elders in dit bestand) om de waarde te binden. Deze hele Actions-tab was het ontbrekende stuk voor P1-9/P1-15's knop-condities — bij een volgende poging op die taken eerst hier proberen i.p.v. de Insert-Widget/Wrap-Widget-routes.

    Generate Dynamic Children — hoe je een ListView/GridView/ StaggeredView een rij per API-response-item laat tonen (gevonden 2026-08-14, bevestigd werkend via P1-7 Tab 3). Dit is niet hetzelfde als de "Backend Query" database-icoon-optie (die haalt alleen de data op via een FutureBuilder, genereert geen herhaalde kinderen) — het is een apart, makkelijk over het hoofd te ziene 4e icoontje in dezelfde kleine iconenrij als hierboven (paint-brush, Actions-node, database, dít 4e icoontje = sliders/kolommen-symbool, play, save-as-template) — hover toont tooltip "Generate Dynamic Children" ter bevestiging. Volledige flow:

    1. Voeg eerst een Backend Query (database-icoon) toe aan de ListView/GridView zelf, type "API Call" → kies de call.
    2. Voeg een item-template-widget toe als kind (bv. ListTile, of een Row/Container naar smaak) via Insert Widget.
    3. Selecteer de ListView/GridView zelf opnieuw → klik het 4e icoontje (Generate Dynamic Children)Variable Name (vrije naam, wordt de loop-itemvariabele) → Value → klik het icoontje naast "Value" → Set from Variable → kies de API-call-response als bron → Available Options → "No Further Changes" (géén Predefined Path/JSON Path hier — je wil de rauwe array, niet één geëxtraheerd veld) → Confirm → Save. Een bevestigingsdialoog volgt ("This widget will now generate its children dynamically... edit the first child to set how every child should look") → Ok. De canvas toont meteen N herhaalde design-time placeholder-rijen.
    4. Cruciaal restpunt: de velden die je vóór stap 3 al op het item-template gebonden had (bv. via de Backend Query direct) wijzen nog naar de hele respons, niet naar het loop-item — moeten opnieuw gebonden worden: Set from Variable → bron is nu de nieuwe loop-itemvariabele (naam uit stap 3, verschijnt als eigen Source-regel) → Available Options → "JSON Path" → pad relatief aan het item (dus $.veldnaam, niet $[:].veldnaam zoals de al bestaande establishmentX-getters in api_calls.dart gebruiken — die zijn geschreven voor de hele array, niet voor een los item).
    5. Voor een tap-actie per item (bv. navigeren met het item's ID): zie de Actions-tab-notitie hierboven — Value van de Navigate-To-parameter ook via Set from Variable → loop-itemvariabele → JSON Path. Geverifieerd via verse export: genereert een correcte ListView.builder(itemCount: ..., itemBuilder: (context, index) { final xItem = xList[index]; ...})-structuur, identiek aan het patroon dat al elders in dit project draait (MasonryGridView.builder/StaggeredView op HorecagelegenhedenOverzicht).

    "+ Add App State Variable"/"+ Add App Constant"-knop reageert niet op Claude's klikken (2026-08-09). Op zowel ?tab=appValues&appValuesTab=state als ...=constant (bereikbaar via ⌘+K → typ een bestaande App State-naam → Enter, betrouwbaarder dan de linker-navigatierail klikken — zie hieronder): een klik op de knop opent geen dialoog, voegt geen rij toe (geverifieerd via de paneel-eigen zoekbalk erna — 0 resultaten), en blind doortypen na de klik komt nergens terecht. 6+ pogingen (directe klik, exacte pixel-herpositionering op zowel tekst als "+"-icoon, klik+wachten 2s, klik+blind-typen) — geen resultaat, geen zichtbare foutmelding. Navigeren tussen "App State"/ "Constants" in de linkerrail en het zoekveld typen werkten in dezelfde sessie wél probleemloos, dus dit is geen algemene klik-doodheid van de pagina, specifiek deze twee "Add"-knoppen. Los probleem: mogelijk gerelateerd aan de bekende Confirm-freeze-patronen elders in dit bestand, maar hier verscheen zelfs geen gedeeltelijke dialoog om vast te lopen — niets gebeurde. Ook: de rij-✏️-edit-iconen op bestaande App State-velden reageerden even onbewogen. Nog niet geprobeerd: browser-zoom (blijkt via keyboard-shortcut geblokkeerd, "page zoom keyboard shortcuts are not supported" — alleen de zoom-tool voor screenshots, niet de pagina zelf); Bob's eigen browser (geen bekende clipping daar). Vuistregel voor volgende sessie: nieuwe App State-velden/Constants zijn voor Claude niet zelfstandig toe te voegen — na 1-2 pogingen overdragen aan Bob met exacte veldnaam+type-specificatie (kost hem seconden per veld).

    Een Drupal Services views-JSON leidt zijn sleutelnaam af van het views-LABEL, niet van de veldnaam — en dat is een veelgemaakte bron van "waarom is dit veld altijd null". Bevestigd 2026-09-04: de app las $[:].bezorgdin terwijl de respons bezorgtin heet, omdat het label van field_hor_bez_bezorgd_in_1 in de view op Bezorgtin (met een t) staat. getJsonField geeft dan gewoon null en de widget toont op élke node de tekst "null" — dus niet alleen bij lege data, wat het makkelijk laat verwarren met een ontbrekende Visibility-guard. Vuistregel bij een veld dat altijd leeg blijft: haal één record met echte data op (curl ... | jq) en vergelijk de sleutelnamen letterlijk, in plaats van de veldnaam uit Drupal over te nemen.

    Een views-veld van het type "All taxonomy terms" of een multi-value referentie komt als LIJST binnen, en .toString() daarop geeft blokhaken. De app toonde [Afhaal, Bezorgen] i.p.v. Afhaal, Bezorgen. FlutterFlow heeft geen ingebouwde join-transform voor een JSON-lijst (de "Available Options"-lijst kent alleen List Contains Item, Filter, Item at Index, e.d.). Gebruik de custom function lijstAlsTekst(dynamic items) die hiervoor in custom_functions.dart staat — die is bewust plat geschreven (geen .map(), geen lokale closure) omdat FlutterFlow's validator daarover struikelt. Let op: of een veld een lijst is zie je niet aan de formatter in de view-export; taxonomy_term_reference_plain levert een string bij single-value (plaats) en een lijst bij multi-value (bezorgtin). Alleen een echt record met data geeft uitsluitsel.

    Custom Code-editor: FlutterFlow genereert de functiesignature zelf — plak alléén de body. Bevestigd 2026-09-04: een compleet uitgeschreven functie (String lijstAlsTekst(dynamic items) { ... }) in het codeveld plakken levert een functie in zichzelf op (String? lijstAlsTekst(...) { String lijstAlsTekst(...) { ... } }) — de buitenste heeft dan geen return en geeft dus altijd null terug. Het glipt langs de validator (terwijl datzelfde geneste-functie-patroon elders juist "The function is empty or cannot be parsed" geeft) en komt gewoon in de export. Herstel via Cancel → "Discard new custom code?" → Yes en opnieuw beginnen; achteraf bewerken is onbetrouwbaar (zie het selectie-verwijderen-probleem elders in dit bestand).

    App Settings → Initial Page: "Entry Page" is de tak voor UITGELOGDE bezoekers, "Logged In Page" die voor ingelogde. Makkelijk te verwisselen, met een groot effect: 2026-09-04 kreeg daardoor juist de nieuwe, uitgelogde bezoeker een verplicht keuzescherm als eerste indruk. Genereert in nav.dart op twee plekken hetzelfde ternaire patroon (de _initialize-route én de errorBuilder) — controleer ze allebei. Beide op dezelfde pagina zetten mag: FlutterFlow toont dan "Warning: Entry Page and Logged In Page should be different", maar dat is een generiek advies, geen fout — de waarde slaat gewoon op (geverifieerd via verse export).

    Emoji/4-byte tekens die als ? in de database staan: kijk naar wat er NAAST de vraagtekens overleefde. Blijft er een losse variatieselector (U+FE0F) of ZWJ (U+200D) staan, dan is het MySQL utf8 (3-byte) i.p.v. utf8mb4: 4-byte sequenties worden bij het opslaan door ? vervangen, 3-byte tekens overleven. Dit raakt ook "fancy" letters uit het mathematical-alphanumeric-blok (U+1D400). De data is dan definitief weg — niets te decoderen, geen app-fix mogelijk. Snelste test of het nú nog gebeurt: een testnode met een emoji opslaan en herladen. Let op dat een tweede schrijfroute (bv. een Django-importer met een eigen databaseverbinding) zijn eigen charset-instelling heeft, ook al staat de kolom goed.

    Na een deploy van custom-module-code die een views-JSON aanpast: de oude respons blijft uit Drupal's page cache komen — test met een cache-buster. Bevestigd 2026-09-08 (P1-45): de code stond correct op productie, maar curl gaf onveranderd de oude vorm terug. De header x-drupal-cache: HIT verraadt het (let op: cache-control: no-store, no-cache staat er óók bij en is misleidend — dat gaat over de client, niet over Drupal's eigen cache). Een willekeurige extra query-parameter maakt een andere cache-key en toont meteen de verse render:

    curl -s -u bob:serhii "<url>&_cb=$RANDOM$RANDOM" | jq -c '.[0]'
    

    Een Cache-Control: no-cache-header doet dit niet — die wordt genegeerd. Belangrijk gevolg: de app vraagt zonder cache-buster op, en krijgt dus de oude respons tot de cache echt geleegd is — een geslaagde cache-buster-test is dus wél bewijs dat je code klopt, maar géén bewijs dat de app het al ziet.

    Bij het testen of een views-endpoint een filter-parameter accepteert: stuur altijd óók een verzonnen parameter mee als controle. Drupal Views negeert onbekende query-parameters stil — zonder die controle bewijst "de resultaten veranderen niet" niets (het kan net zo goed de verkeerde parameternaam zijn). Bevestigd 2026-09-05: onzin_param=123 gaf exact dezelfde 25 items als vijf serieuze kandidaat-filternamen, wat pas hard maakte dat er geen exposed datumfilter bestond.

    Domein/architectuurcontext

    • Merk-DNA van uitgaanskrant.com (bron van waarheid voor look&feel; uitgelezen 2026-08-30 uit de live CSS + berekende stijlen). Vier kleuren met elk één vaste rol, nergens door elkaar gebruikt: #9A141D logo-rood = identiteit, #FF680D oranje = actie (links, menubalk, actieve pagina, knoppen), #3E454C leisteen = chroom én standaard tekstkleur, #09B34A groen = categorielabel. Verder: #EE3338 locatiespeldje, #F2F2F2 kruimelpad/rustvlakken. Typografie: Roboto voor interface (body 14px, sectiekoppen 16px 600 in kapitalen), Droid Serif bold voor inhoudelijke titels (Noto Serif is de directe opvolger in Google Fonts). Componenten: witte kaart met 3px #EEE rand, geen afronding en geen schaduw; categorielabel is een recht groen blokje (12px, padding 0 7px), geen pil; Font-Awesome-icoon vóór datum en adres; kruimelpad op elke detailpagina. Vuistregel bij twijfel: rood is merk en chroom, oranje is wat je aanklikt om verder te komen — de site gebruikt rood nóóit als knopkleur.

    • Het app-thema draagt dit merk-DNA inmiddels volledig (P1-32 t/m P1-34, P1-39 en P1-40 afgerond 2026-08-31). LightModeTheme in lib/flutter_flow/flutter_flow_theme.dart: primary #9A141D (logo-rood), secondary #FF680D (oranje/actie), tertiary #09B34A (groen/categorie), primaryText #3E454C (leisteen), alternate #EEEEEE, primaryBackground #F2F2F2. Alleen #EE3338 (locatiespeldje) heeft nog geen eigen token; de vier Accent-slots staan nog op FlutterFlow-fabriekswaarden en zijn dus vrij als je er een nodig hebt (Theme Settings → Colors → "+ Add Color" maakt ook een eigen benoemde ingang). In levende code staat geen hardcoded rood meer — wat er nog aan #B1061E/#B50808/#B12727 in de export zit, zit uitsluitend in dode kopieën (_copy, _copy2, copymethartje, kanweg/*) en wacht op P2-7's opruimactie.

    • **flutterflowmobiel1 levert categorie NIET in één vorm: displays services_1/2/3 geven een lijst (["Kindvriendelijk","Theater"]), services_4/5/6/7 een komma-string ("Kindvriendelijk, Theater").** Gemeten 2026-09-06 op pagina 0 van alle zeven displays. Alles wat over dit veld itereert werkt daardoor op de ene tab wel en op de andere niet. Zie TASKS.md P1-45; de nette oplossing zit in de view, niet in de app.

    • Firebase App Check beschermt alleen Firebase-diensten zelf (Firestore/Cloud Functions/Storage), niet de eigen Drupal-backend van dit project. Besproken 2026-08-25 (Bob's vraag n.a.v. FlutterFlow's App Check-toggle bij het instellen van Crashlytics, zie P1-16/P1-30): de app praat rechtstreeks met een custom Drupal-Services-API, geen Firebase-dienst — App Check aanzetten in FlutterFlow doet dus niets voor die API, tenzij er óók custom Drupal-code bij komt die het App-Check-token (Play Integrity/DeviceCheck) verifieert. Niet gebouwd (te veel moeite voor een nog ongemeten risico) — zie P1-30 voor het gekozen alternatief (Cloudflare rate limiting). Kernbeperking die hierbij ook geldt en niet op te lossen is: alles wat de app meestuurt (URL's, headers) staat letterlijk in de gecompileerde app en is te achterhalen — geen enkele edge-laag (Cloudflare of anders) kan cryptografisch bewijzen "dit request komt echt van de app" zonder een App-Check-achtige attestatie-oplossing met backend-verificatie.

    • Drupal 7 Services-module: een nieuwe custom resource moet ook los geactiveerd worden per endpoint — code alleen is niet genoeg. Bevestigd 2026-08-19 (Bob + Claude samen gedebugd, P1-7 Tab 1 "Persoonlijke agenda"): favorieten_agenda.json gaf op productie een kale, lege 404 (geen enkele Drupal-header, niets in de watchdog-log), terwijl exact dezelfde custom.module-code op devbob prima werkte. De resource zelf stond correct gedefinieerd in hook_services_resources() (custom_services_resources() in custom.module) — dat alleen registreert 'm bij de module, maar een Services-endpoint (bv. flutterdrup) serveert alleen de resources die daar expliciet voor zijn aangevinkt, een aparte config/database-instelling per endpoint, los van de module-code. Die aanvinking was ooit op devbob gedaan (tijdens testen) maar nooit meegenomen naar productie. Check/fix altijd hier: https://<domein>/en/admin/structure/services/list/<endpoint-naam>/resources (of via drush @<site-alias> php-eval "print_r(services_endpoint_load('<endpoint-naam>'));" — vergelijk de resources-array tussen omgevingen). Symptoom dat hierop wijst: een endpoint/pad dat op de ene Drupal-omgeving prima werkt en op de andere een 404 geeft mét een compleet lege body en zonder de gebruikelijke custom response-headers (bij dit project bv. x-speed-cache/x-device/x-geoip-*, toegevoegd door de eigen Aegir/Boa-caching-laag pas ná volledige Drupal-bootstrap) — dat wijst op "wordt vroeg afgewezen, bereikt zelfs Drupal's eigen watchdog-log niet", eerder dan een nginx/Cloudflare-routeringsprobleem. Nuttig debug-recept dat hiertoe leidde: (1) drupalRequest's eigen print()-debug-output opvangen via adb logcat tijdens een live test in de app, om de exacte headers/status/body te zien die de app zelf verstuurt/ontvangt; (2) diezelfde request los met curl -i natesten op beide omgevingen; (3) om Cloudflare écht te omzeilen moet je letterlijk 127.0.0.1 in de curl-URL zetten (niet de echte hostnaam, ook niet met een aangepaste Host-header — Cloudflare zit op DNS-niveau, een Host-header alleen stuurt de request nog steeds via Cloudflare's edge); (4) menu_router-tabel checken via drush php-eval (leeg voor beide omgevingen was hier een belangrijke aanwijzing dat dit geen kaal hook_menu()-pad was maar via Services liep); (5) de Drupal watchdog-log (admin/reports/dblog) vergelijken tussen omgevingen — als de kapotte omgeving zelfs geen enkele logregel toont terwijl een ander, vergelijkbaar endpoint op diezelfde omgeving wél volledig doorloopt (inclusief juiste gebruikersherkenning), sluit dat sessie/cookie/nginx-routing- problemen vrijwel uit en wijst het naar iets resource-specifieks binnen Drupal/Services zelf.

    • De app is bewust licht-only (besluit Bob, 2026-08-31). Het dark-mode-thema is uitgezet in Theme Settings → Colors (schakelaar "Dark Mode Theme"), omdat uitgaanskrant.com zelf ook puur licht is en een tweede palet onderhouden geen basis had. Gevolg in de export: de DarkModeTheme-klasse bestaat niet meer, MaterialApp krijgt alleen theme: ThemeData(brightness: Brightness.light) en géén darkTheme, dus themeMode: ThemeMode.system valt voor iedereen terug op licht — ongeacht de toestelinstelling. Praktisch gevolg voor toekomstig werk: je hoeft geen enkel scherm in twee varianten te testen, en een hardcoded lichte kleur (Colors.white e.d.) is geen latent dark-mode-probleem meer. Zet dit niet terug aan zonder eerst de ~110 hardcoded kleuren in de live widgets aan thema-tokens te hangen.

    • Taal/locale volgt het toestel, niet hardcoded Nederlands — zonder opgeslagen voorkeur (FFLocalizations/_kLocaleStorageKey) valt MaterialApp.locale terug op de systeemtaal van het toestel; matcht die met 'en' (tweede taal in FFLocalizations.languages() = ['nl','en']), dan draait de app in het Engels. Er is vrijwel geen echte Engelse vertaling (zie TASKS.md P1-17, live bevestigd 2026-08-07: van 169 sleutels in lib/flutter_flow/internationalization.dart zijn er maar 2 waar nl/en daadwerkelijk verschillen, en één daarvan is een fout i.p.v. een vertaling). Test dus altijd met het testtoestel/de emulator op Nederlands (adb shell getprop ro.product.locale checken), anders lijkt de UI kapot terwijl het eigenlijk gewoon de ontbrekende Engelse vertaling is.

    • Drupal 7 Services sessie-auth: sessid + session_name (uit LoginCall's response) vormen samen de sessie-cookie: Cookie: <session_name>=<sessid>. token is een los CSRF-token, alleen nodig bij schrijf-requests (POST/PUT/DELETE), nooit bij GET.

    • Handmatig curl-testen tegen Drupal Services: voeg altijd HTTP Basic Auth toe (curl -u <user>:<pass> ...), zowel op devbob als productie. Bevestigd 2026-08-26: zonder deze -u-vlag krijg je geen JSON terug maar een kale challenge-/foutpagina die op het eerste gezicht als een Cloudflare-blokkade oogt (bevatte zelfs Cloudflare-JS-snippets) — bleek in werkelijkheid een server-niveau HTTP Basic Auth-laag vóór Drupal, losstaand van de gewone Drupal-sessie-login hierboven. Kostte een aantal verwarrende rondes voordat dit duidelijk werd; voortaan gewoon altijd meegeven. Credentials staan niet hier (git-bestand) maar in de lokale Claude-projectmemory.

    • Drupal page cache bootstrapt vóór de sessie — een ooit anoniem gecachete response (bv. een 403) kan session-bootstrap, hooks én custom-module-logging volledig overslaan. Bij twijfel: Drupal-cache legen en opnieuw testen vóór verder debuggen.

    • Programmatisch een node aanmaken (node_save() buiten het website-formulier om, bv. via een custom Services-actie): zet $node->language op 'nl', niet op LANGUAGE_NONE. Bevestigd 2026-08-26 (v2 evenementen/stadsactiviteiten-aanmaak, zie TASKS.md P2-15): LANGUAGE_NONE leek logisch omdat veld-waarden toch al onder LANGUAGE_NONE/['und'] staan, maar het node-taal- attribuut zelf hoort op deze i18n-bewuste site op 'nl' te staan (bestaande content staat op "Nederlands", nooit op "Taalonafhankelijk"). Met LANGUAGE_NONE crashte elke pagina die de nieuwe node toont/bewerkt met Attempt to assign property "calendar_date_link" on null in calendar_plugin_row->pre_render() — Date Views' $view->date_info- opbouw bleek afhankelijk van de node-taal. Alle overige velden (datum, adres, geo, stad) waren steeds al correct; dit was zuiver het language-attribuut. Geldt generiek voor toekomstig programmatisch node_save()-werk op deze site, niet uniek voor één endpoint.

    • Views numeric filter: $view->filter['uid']->value moet array('value' => $uid) zijn, niet array($uid) — de foute vorm faalt stil (geen filter toegepast) i.p.v. een error te geven.

    • Provincie/gemeente blijft het basismodel voor content-scoping (bevestigd: mensen zoeken primair lokaal). Home is de landelijke standaard-startpagina zónder verplichte gemeente-keuze vooraf — dat vervangt het provincie/gemeente-model niet, het is een aanvullende ingang. De Home-prefixed componenten (HomeUitgaantabelKaartComponent e.d.) zijn een bewuste kopie van PUitgaanSliderComponent/ UitgaantabelKaartComponent + een eigen cityid-loze API-call — geen dode code, niet meenemen in opschoonacties.

    • City-editor-stadsrechten (field_town_access) zijn sinds 2026-08-27 gemeente-niveau, niet meer plaats-niveau — taxonomie zelf (Provincie→ Gemeente→Plaats, vid 5) is en blijft 3 niveaus, alleen de rechten-laag is versimpeld. custom_field_widget_form_alter() in custom.module (het enige stuk code dat field_town_access afdwingt, voor het field_act_state_municipality_tow-veld op activity) leest de opgeslagen tids nu als gemeenten en zoekt daar de kind-plaatsen bij op (taxonomy_term_hierarchy, h.parent IN (...), vid = 5) i.p.v. een letterlijke tid-match — repareert meteen de oude array-vs-scalar-bug (array_column(...,'tid') i.p.v. het hele veld doorgeven aan condition('tid', ...)). Bewust niet de taxonomie zelf platgeslagen naar 2 niveaus: de Flutter-app toont de Plaats-naam wél degelijk aan eindgebruikers (horeca-kaarten, evenement-infopagina, fallback 'Plaats onbekend'), dus dat zou zichtbare precisie kosten + een bulk-hertag-migratie vereisen. Volledige analyse + de patch staan in het levende Artifact, zie [[drupal-events-inventory-artifact]].

      • Het veld field_town_access gebruikt widget term_reference_tree (vendored contrib-module) — om een admin bij het instellen van een City-editor alleen gemeenten te laten aanvinken (geen Plaats-niveau meer te zien/aan te vinken), moet je op dat veld (Manage Fields → potlood-edit) de widget-instellingen aanpassen: "Leaves only" uitvinken (anders krijgt alleen een knooppunt zónder kinderen — dus Plaats — een vinkje; Provincie/Gemeente blijven dan puur uitklap-navigatie zonder checkbox) én "Maximum Depth" op 2 zetten (Provincie=niveau 1, Gemeente=niveau 2, Plaats=niveau 3 in deze widget's telling — met 2 wordt Plaats niet meer opgebouwd/ getoond). Geen "minimum depth"-optie beschikbaar in deze widget, dus een Provincie-rij krijgt na het uitvinken van "Leaves only" ook een vinkje — puur een kwestie van zelf alleen op gemeente-niveau aanvinken, geen harde blokkade tegen per ongeluk een provincie kiezen (de code hierboven gaat er dan wel gewoon vanuit dat het een gemeente is en zoekt de kind-termen op).
      • Widget-instellingen wijzigen raakt nooit al opgeslagen waarden — alleen het bewerkformulier. Oude, al-opgeslagen plaats-tids in field_town_access verdwijnen niet vanzelf door de instelling te wijzigen; je moet het account echt opnieuw opslaan. Bevestigd via _term_reference_tree_widget_validate(): bij het opslaan wordt het veld volledig herbouwd uit alléén wat op dat moment daadwerkelijk gerenderd is in de boom (geen verborgen-oude-waarde-passthrough) — dus zodra Max depth = 2 staat en je het account opnieuw opslaat (ook zonder verder iets aan te raken), vallen niet-gerenderde plaats-tids er vanzelf uit. Sneller/rechtstreeks via drush, zonder door de UI te hoeven: taxonomy_get_parents_all($tid)-lengte === 3 identificeert een Plaats-tid (dezelfde check die custom.module zelf ook gebruikt voor de vergelijkbare ?city=-prefill-guard, zie hieronder) — filter die eruit en user_save().
      • De "Nieuw stadsactiviteit"-snelkoppelingen (View places, block "Towns Owned By User", reverse-relatie op field_town_access) blijven werken zonder wijziging — die view is volledig niveau-agnostisch, toont gewoon elke term die aan de gebruiker gekoppeld is. Enige neveneffect: de link stuurt ?city=<tid> mee, en custom_form_activity_node_form_alter() vult het "Plaats"-veld alléén automatisch voor als count(taxonomy_get_parents_all($tid)) === 3 (dus alleen bij een echte Plaats-tid) — bij een gemeente-tid slaat de auto-invulling gewoon over (geen crash), en moet de gebruiker de plaats 1x zelf uit de (al correct beperkte) dropdown kiezen.
    • Favorieten/login zijn P0. Backend-endpoint bestaat al; Drupal 7 views die de respons voeden hebben soms nog aanpassing nodig.

    • Fout-/leeg-afhandeling bij API-calls: het "geen enkele FutureBuilder checkt op meer dan !snapshot.hasData"-beeld klopte niet helemaal (gecorrigeerd 2026-08-04). ApiCallResponse vangt netwerkfouten zelf op (api_manager.dart, catch (e) => ApiCallResponse(null, {}, -1, ...)) — de Future voltooit dus altijd, geen oneindige spinner. Het eigenlijke risico was een synchrone crash: bij jsonBody: null gooit getJsonField(null, ...).toList() een NoSuchMethodError vóórdat de lijst-widget ooit gebouwd wordt. Bob heeft hiervoor op HorecagelegenhedenOverzicht een werkend patroon gebouwd (2026-08-04, rechtstreeks in de builder): bij lege/mislukte data toont elke tab nu een standaardplaatje i.p.v. te crashen. Exacte builder-stappen (welke widget/property) nog niet gedocumenteerd — bij het uitrollen naar andere pagina's (zie TASKS.md P1-1) eerst navragen/naspeuren i.p.v. blind het Carousel-"Empty List Widget"-patroon hierboven te kopiëren, want dat lost alleen itemCount: 0 op, niet per se de null-jsonBody-crash die hier de kern van het probleem was.

    • Custom action drupalRequest (lib/custom_code/actions/drupal_request.dart) geeft sinds 2026-08-17 bij elke fout (non-2xx, timeout, exception) een lege [] terug, niet meer een {'success': false, ...}-map. Root cause was een crash op Favorieten (zie TASKS.md): aanroepende widget-code doet altijd blind .toList() op het resultaat, en .toList() op een Map bestaat niet (NoSuchMethodError). Er bleek nergens in de app een consument die de oude success/statusCode/ body-velden van de foutmap daadwerkelijk uitlas (gecheckt vóór de wijziging), dus dit is veilig gewijzigd voor alle 3 huidige gebruiksplekken (favorieten_widget.dart x2, lib/kanweg/). Bij een nieuwe aanroep van drupalRequest toevoegen: ga ervan uit dat je bij een fout een lege lijst terugkrijgt, geen foutdetails — wil je die wél (bv. om een eigen foutmelding te tonen), dan moet de functie-contract opnieuw aangepast worden en alle bestaande aanroepers gecontroleerd.

    • drupalRequest is géén centraal punt om lege/null-veldwaarden op te vangen voor de meeste widgets — bevestigd 2026-08-19 (Claude, grep -rn "drupalRequest(" lib/). Deze custom action wordt alleen aangeroepen vanuit favorieten_widget.dart (2x) en het dode lib/kanweg/. Vrijwel alle andere data (o.a. alles achter P1-6's valueOrDefault<String>(<bron>, '<placeholder>')-patroon, zoals HorecagelegenheidoverzichtKaart/evenement_info_widget.dart) komt binnen via FlutterFlow's ingebouwde declared API Calls (api_calls.dart, bv. HorecagelegenheidoverzichtCall) — die lopen nooit langs drupalRequest, en die response heeft geen centraal transform-punt: elk veld wordt los, inline via getJsonField(...) op zijn eigen plek in de widget-code uitgelezen. Een "lege string → spatie"-fix in drupal_request.dart zou dus alleen de 3 Favorieten/kanweg-aanroepen raken — voor de rest blijft de per-widget Default Value-route (zie P1-6) de enige praktische fix.

    • API-call cache: true werkt functioneel correctApiCallOptions (lib/backend/api_requests/api_manager.dart) extends Equatable met params/headers in de props-lijst, dus de in-memory _apiCache vergelijkt op echte parameterwaarden (deep equality), niet op object-referentie. Geen reference-equality-bug om je zorgen over te maken — cache: true is een veilige, lichte performance-fix voor calls met effectief statische data binnen een sessie (referentie- lijsten zoals provincies/gemeenten, categorieën). Lost wél alleen herhaalde identieke calls op (bv. bij een rebuild), niet een eerste gelijktijdige burst van meerdere verschillende calls (bv. een eager TabBarView met 6+ tabs die elk hun eigen data ophalen bij page-load, zie TASKS.md P1-10).

    Samenwerken met Bob

    • ⚠️ Claude verwijdert NOOIT iets — staande regel, Bob expliciet 2026-09-04. Geen componenten of pagina's weggooien in de builder, geen git rm, geen bestanden verwijderen, ook niet als ze aantoonbaar dood zijn én Claude ze zelf als dood heeft geïnventariseerd. Opruimwerk hoort altijd op Bob's naam in TASKS.md; Claude levert hooguit de geverifieerde lijst + de reden per regel. Dit geldt ook wanneer Bob in een eerder bericht van dezelfde sessie "ja" zei tegen iets anders — toestemming voor bouwen is geen toestemming voor weggooien.

    • Geef Bob altijd 2-3 taken tegelijk, nooit één (staande regel, Bob expliciet 2026-09-08). Hij werkt door terwijl Claude verifieert; met één taak per keer staat hij stil te wachten op een controle. Lever dus een klein blokje waar hij zelf een volgorde in kan kiezen, en het liefst uit hetzelfde gebied (bv. drie dingen in dezelfde Drupal-view).

    • Nummer taken doorlopend binnen één chat (staande regel, Bob expliciet 2026-09-08). Elke taak die je geeft krijgt een eigen volgnummer dat je in die chat niet hergebruikt, ook niet als een taak uit meerdere stappen bestaat — dan splits je 'm in aparte nummers. Verwijs bij een antwoord/verificatie altijd naar dat nummer, zodat er geen misverstand is over welke taak bedoeld wordt. Dit staat los van de TASKS.md-ID's (P1-42 e.d.), die blijven gewoon leidend voor het dossier; het chat-nummer is puur de gespreksdraad.

    • Bob is de enige developer/eigenaar, werkt vaak zelf gelijktijdig in dezelfde builder-sessie. Neem niet aan dat elke wijziging van jou komt.

    • Prioriteit: "eerst een werkende app live, daarna features" — P0 weegt zwaar boven P1 en P2. Bob herprioriteert soms fors zelf (bv. login+favorieten van P2 naar P0) — volg dat exact.

    • Claimt Bob een taak terug ("dat regel ik zelf") — stop daar direct mee en pak iets anders onafhankelijks op.

    • Kost iets veel tijd door handmatige builder-acties (vastzittende dialogen, geclipte controls): na 1-2 serieuze pogingen stoppen en concreet aan Bob voorstellen dat hij het zelf doet (exacte stappen).

    • Rapporteer nieuw gevonden bugs (vooral op een pagina waar Bob net zelf op zit) direct en duidelijk, niet pas in een latere samenvatting.