CLAUDE.md 23 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. (Precedent 2026-08-04: twee sessies pakten onafhankelijk dezelfde P0-taak op — Bob moest scheidsrechteren tussen twee stukken werk aan hetzelfde bestand.)

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.

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).
  • 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).
  • Check altijd eerst met adb devices of er een device begint met emulator* vóórdat je het script start — Bob's emulator staat niet altijd aan, en er kan een ander (fysiek) device aangesloten zijn dat niet bedoeld is om op te draaien. Ontbreekt emulator-5554: navragen bij Bob i.p.v. blind op het eerste beschikbare device te draaien.
  • 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).

Daarna automatisch:

  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.)

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

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. 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.

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.

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).

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.

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.

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.

Domein/architectuurcontext

  • 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.
  • 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.
  • 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.
  • 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.

  • 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

  • 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.