*Opgeschoond 2026-09-19 (van 1924 naar ~800 regels). Afgeronde taken zijn
verwijderd, niet gearchiveerd — ze staan in de git-historie en de blijvende
inzichten in CLAUDE.md. Alles hieronder is open werk. Staat iets er niet
meer in, dan is het klaar of afgewezen — niet opnieuw agenderen.*
Voor een nieuwe sessie: pak een taak uit "Claude — open", zet er meteen
— bezig achter in dit bestand (een echte Edit, niet pas bij het afsluiten;
andere sessies delen deze working directory), en verifieer elke builder-wijziging
met een verse export. Taken onder "Bob — open" niet zelf oppakken.
De app-kant is vrijwel af. Op 2026-09-19 zijn o.a. afgerond: de
header-locatie-keten (Home toont Heel Nederland, provinciepagina de
provincienaam), het adaptive icon, de loginpagina, het drawer-logo, Crashlytics,
"Mijn aanmeldingen", de categoriedropdown op het horeca-overzicht, de
horeca-agenda-entiteiten en de opruimronde. Op 2026-09-20 kwamen daar het dichtgezette API-oppervlak (42 → 22 methodes,
het name-lek weg) en de feature graphic bij. Wat rest is livegang-werk:
Google Play en één doorloop die alleen Bob kan doen.
| # | Taak | Kort |
|---|---|---|
| 137 | Lege-lijstmelding flitst vóór de API-respons (Favorieten tab 1-3, horeca-overzicht 6 tabs) | Bob (gekozen 2026-09-24, stappen hieronder). Oorzaak: de lijst leest een page state die pas in On Page Load / tab-OnTap gevuld wordt; de eerste build ziet [] en toont de Empty List Widget tot de fetch klaar is. Pagina's met een Backend Query op de lijst zelf hebben dit niet (spinner). Fix = boolean …Geladen die pas NÁ de Update Page State op true gaat, en de lijst daarop verbergen. A · horecagelegenhedenOverzichtCurrent: (A1) per Tab-node Cultuur/Eetgelegenheden/Overnachten/Uitgaan/Verhuur-catering → Actions → On Tap → Edit: de eerste actie Update Page State tabXGeladen = true weghalen (⋮ → Delete Action) en als laatste actie opnieuw toevoegen (+ onder het laatste blok → State Management → Update Page State → tabXGeladen → Set Value true). (A2) pagina-root → State Management → + Add Field tabActiviteitenGeladen Boolean, Initial Value expliciet false (toggle 2×). (A3) Scaffold → On Page Load → Edit → + na de laatste actie → Update Page State tabActiviteitenGeladen = true. (A4) eerste Column onder de eerste TabBar Page → Wrap Widget → ConditionalBuilder → ConditionalBuilder-rij → Conditions → Unset → Page State → tabActiviteitenGeladen (zelfde patroon als tab 2-6). B · Favorieten: (B1) pagina-root → State Management → 3× + Add Field, Boolean, Initial false: agendaGeladen, gemeentenGeladen, horecaGeladen. (B2) Scaffold → On Page Load → Edit → + na Update Page State favorietenAgenda → Update Page State agendaGeladen = true. (B3) Tab-node 2 (gemeenten) → On Tap → Edit → + na Update Page State favorietenGemeenten → gemeentenGeladen = true; idem Tab 3 (horeca) na favorieteHorecagelegenheden → horecaGeladen = true. (B4) Visibility → Conditional op de lijst-widget van elke tab (tab 1 StaggeredView onder de Expanded, tab 2 ListView, tab 3 StaggeredView) → Unset → Page State → de bijbehorende boolean (rechtstreekse boolean, geen Single Condition). Verifiëren (verse export): horeca: grep -n "Geladen = true" …overzicht_current_widget.dart — elke regel hoort NÁ de _model.alleX =-regel van dezelfde keten; grep -c "if (_model.tab.*Geladen)" = 6. Favorieten: grep -n "Geladen" favorieten_widget.dart toont 3× = true na de Update Page State en 3× if (_model.xGeladen) om de lijsten. Baseline-export van vóór de wijziging stond in /tmp/ff-check137a |
| FOTOBOEK | Fotoboek: foto's kiezen uit Drupal + beheren | Claude — bezig (2026-09-22/23). Drupal AF. App: alle VIER de upload-knoppen zijn om en export-geverifieerd (logo + foto's op uitgaansevenementAanmaken, logo + foto's op stadsactiviteitAanmaken), dart analyze 0 errors. Elke knop: Confirm Dialog ("Waar komt het logo vandaan?" / "Waar komen de foto's vandaan?", knoppen Mobiel / Fotoboek) → conditional op confirmDialogResponse → TRUE = bottom sheet 85 % met FotoboekGridComponent, FALSE = de bestaande upload-keten. Bij de logo-knoppen zet de callback createLogoFid/createLogoUrl, bij de foto-knoppen doet hij Add to List op createFotosFids/createFotosUrls — dus meerdere keuzes vullen aan, net als de mobiele kiezer. ⚠️ Wegklikken van de dialoog telt als Mobiel (FlutterFlow genereert ?? false). Nog te doen: (a) 5e tab "Mijn foto's" op mijnProfiel voor beheer/verwijderen (endpoint mijn_fotos + mijn_fotos/verwijder staan live); (b) cosmetica op het component: zoekveld boven de grid i.p.v. eronder (2 sleeppogingen mislukt), Hint Text "Zoek op bestandsnaam" + EN-vertaling; (c) testen op een toestel — Claude kan dat niet, want alle vier de pagina's vragen om ingelogd zijn. Opruimen (Bob): wegwerp-component FotoboekGridComponentCopy, en het callback-argument urlTekst op FotoboekGridComponent (door Claude toegevoegd toen createFotosUrls nog een List<String> leek; het is een List<Image Path>, dus url volstond — het argument is onschadelijk maar ongebruikt). Endpoint-spec + builder-recepten staan in CLAUDE.md |
| G0.6 | Play-screenshots | ✅ Nieuwe set van 10 klaar (2026-09-19 avond). Alleen tel-04-horeca.png moet over zodra Show Test Ads uit staat |
| 114 | Opruimen na de filterbalk-ombouw | Eigenaar: Bob. Restjes van taak 113 (afgerond 2026-09-20): (a) component-parameter parameter1 op FilterBalkComponent ontstond automatisch bij Convert to Component en wordt nergens gelezen — mag weg, en daarmee ook de meegegeven waarde op Home en PUitgaanPage; (b) page state zoekOpen op Home is overbodig (de App State-versie stuurt alles); (c) App State zoekActief (Boolean) bestond al vóór deze taak en heeft 0 gebruikers; (d) wegwerp: componenten FilterBalkKanweg en FilterBalkComponentCopy + pagina's homeCopy en PUitgaanPageCopy. Geen van alle blokkeert iets — dart analyze geeft 0 errors |
Vraag van Bob: waar komen die vandaan? Geen enkele komt uit recent werk — de teller stond bij de nulmeting van deze sessie op 69 en liep tijdens de sessie terug naar 54; er kwam er geen bij. Warnings hebben ook nooit een export geblokkeerd (alleen errors doen dat, en niet eens allemaal). Vijf soorten:
| Soort | Aantal | Wat het is |
|---|---|---|
| Widget Configuration — "Unique Key for component X is not set" | grootste groep | Het Value Key-veld op een component-instantie. Betreft HorecagelegenheidoverzichtKaart (21 instanties in de export), tagCategorieComponent (8), UitgaantabelKaart (4), PUitgaanSliderKaartComponent (3). FlutterFlow raadt dit aan voor componenten in een lijst. ⚠️ Let op: een Value Key op een component-instantie reset een gepagineerde lijst niet (zie CLAUDE.md), dus dit is een aanbeveling, geen functioneel gebrek |
| Primary Scroll — "Multiple primary scrollable widgets nested within each other" | tweede groep | Geneste scrollables (een Column met Scrollable aan binnen een scrollende ouder). Bekend patroon in dit project |
| Disabled Action — "Action on SelectStateDropDownComponent is disabled" | weg (69 → 54) | Dit was taak 112; tijdens deze sessie door een andere sessie opgeruimd |
| Auth Page Setup — "Logged In Page and Entry Page cannot be the same" | 1 | Beide staan op Home (nav.dart: appStateNotifier.loggedIn ? HomeWidget() : HomeWidget()). Bewuste keuze sinds 2026-09-04, zie CLAUDE.md — FlutterFlow's melding is generiek advies, geen fout |
| Custom Code Dependencies — "deprecated package flutter_html" | 1 | flutter_html in pubspec.yaml (r. 65), gebruikt door de custom widget FromHTML |
Het grootste deel zit in DODE kopie-paginas. Een aangeklikte Primary Scroll-warning
landde op mijnProfielCopy3, en de export bevestigt het patroon:
| levend | dood | |
|---|---|---|
| component-instanties zonder Value Key | 29 | 7 |
SingleChildScrollView (de Primary-Scroll-bron) |
10 | 12 |
Van de levende SingleChildScrollViews is mijnProfiel de enige die ook lijsten bevat
(3 scrollende Columns met 5 ListView.builders erin). Alle andere Primary
Scroll-warnings komen dus uit wegwerp-code: mijnProfielCopy/Copy2/Copy3/Copy4,
home_copy, PUitgaanPageCopy/Copy2/Copy3, favorietenCopy,
selectprovinciegemeenteCopy, plus de componenten FilterBalkComponentCopy,
FilterBalkKanweg en FotoboekGridComponentCopy — 13 stuks, die allemaal al op de
opruimlijst staan (P2-7 / 114 / FOTOBOEK).
Advies: ruim eerst die 13 op. Eén actie, nul risico voor levende code, en daarmee verdwijnt het leeuwendeel van beide groepen. Pas daarna is de rest de moeite waard.
Waarom de meldingen er staan
key.
Zonder key matcht hij op positie+type, dus bij herordenen/filteren kan state bij het
verkeerde item belanden. ⚠️ Verwacht er geen functionele winst van: een Value Key op een
component-instantie reset een gepagineerde lijst niet (zie CLAUDE.md), en deze
kaarten hebben nauwelijks eigen state.PrimaryScrollController per route, en elke scrollable die
primary aan heeft claimt die. FlutterFlow zet Primary standaard aan bij elke Column
met Scrollable; zit daar een ListView in die dat ook doet, dan vechten ze erom.
Merkbaar bij scroll-restauratie en "tik op de statusbalk om omhoog te scrollen".De fix (per plek): widget selecteren -> rechterpaneel -> onder Scrollable de toggle
Primary uit. Zet hem uit op de scrollable die niet de hoofdscroller is — bij een
ListView met shrinkWrap: true in een scrollende Column is dat de ListView. Genereert
primary: false; het enige bestaande voorbeeld in dit project staat in
EvenementInfo (SingleChildScrollView(primary: false)).
Voor Unique Key: instantie selecteren -> sectie Value Key (net boven Component
Properties) -> binden aan het nid van het loop-item (JSON Path $.nid). 29x, dus fors
werk voor weinig opbrengst.
Bereikbaarheidsanalyse op een verse export: vanaf Home de widget-graaf gevolgd.
45 widget-klassen, 36 bereikbaar, 9 wezen. Warnings stonden na Bobs ronde op 44
(was 54).
Groep A — nul verwijzingen, kan direct weg:
FilterBalkKanweg · KaartTabelUitgaanComp · KaartTabelUitgaanSComp ·
PUitgaantabelKaartComponentOrgineelMetKaartjeerin · SliderUitgaanComponentSmallCurrent
Groep B — cluster rond de dode Event-pagina, weg in DEZE volgorde:
Event (heeft wél een route in nav.dart, maar niemand navigeert erheen)EvenementComponent en HeaderCurrent (alleen door Event gebruikt)KaartSliderUitgaanSComp (alleen door SliderUitgaanComponentSmallCurrent uit groep A)⚠️ Event weigert al sinds 2026-08-25 élke wijziging met "Invalid Action: The most
recent action would have caused a crashing error" — hernoemen, verplaatsen én een component
verwijderen gaven alle drie die blokkade (zie CLAUDE.md). Verwijderen kan dus ook
mislukken; 1-2 pogingen, dan laten staan. Geen functioneel risico, de pagina is onbereikbaar.
Bevestigd NIET weg: FotoboekGridComponent (in gebruik via de bottom sheet op beide
aanmaakpagina's), HorecagelegenhedenOverzichtProvinciePage, StadsactiviteitAanmaken,
UitgaansevenementAanmaken.
SelectStateDropDownComponent · On Initialization (20 acties)Vijf stuks, gemeten 2026-09-23: 4, 7, 8, 16, 18.
| # | Actie | Waarom uit |
|---|---|---|
| 4 | Update App State |
leeg — geen velden ingesteld |
| 7 | Show Snack Bar |
leeg — geen bericht ingesteld |
| 8 | Rebuild Component |
volledig, maar dubbelop (actie 6 ervóór en 10 erna forceren al een rebuild) |
| 16 | Custom Action getFavorieteGemeenten |
volledig; actie 17 (drupalRequest, wél actief) lijkt de opvolger |
| 18 | Update Component State |
volledig; 19 erna doet hetzelfde |
4 en 7 kunnen zonder meer weg — een lege actie kan per definitie niets doen, en als je
'm aanzet wordt de warning juist een error. Dát is waarom ze disabled staan.
8, 16 en 18 ook, met één kanttekening: ze zijn inhoudelijk compleet, dus check bij 16
even of drupalRequest (17) de favoriete gemeenten inderdaad overgenomen heeft.
Terzijde over disablen vs. deleten (Bobs werkwijze): een disabled actie genereert geen code, dus in gedrag is er nul verschil met verwijderen — de veiligheid zit puur in het terug kunnen zetten. Datzelfde krijg je gratis van FlutterFlow's eigen versiebeheer: commit vóór het opruimen, dan is deleten even veilig als disablen, en je houdt de Issues-lijst schoon. De prijs van disablen is één warning per actie.
| # | Taak | Waarom nu |
|---|---|---|
| G | Google Play-checklist G0–G4 | Volledige checklist hieronder |
| 65 | De ingelogde doorloop | Kernfuncties zijn nooit op een release-build getest. Claude kan dit niet (geen wachtwoorden) |
| AdMob | Show Test Ads uit |
App Settings → AdMob. Wacht op de Google-registratie |
| P1-17 | Vertalingen | ✅ Rond voor de app (2026-09-20): 0 van de 218 gerenderde sleutels mist nog Engels. Alleen de vier lege-lijst-teksten op mijnProfiel blijven over — dat zijn parameterwaarden, geen i18n-sleutels |
| Drupal | taak 56-restant · taak 28/109 | Cron-description op "xx days" ✅ gedaan · events zonder categorie gaat via de importer |
| Bij livegang | Schakelaars en opruimklussen | Eigen blok hieronder |
| ✅ AF (Bob 2026-09-20, export-geverifieerd): volgorde is nu slider → banner → filterbalk → lijst | ||
✅ AF (Bob 2026-09-20): Container 65/65/105/105 mét de padding erin, dus de banner krijgt zijn volle 50 resp. 90 dp én de lucht tot de slider blijft |
||
| 120 | 11 hardcoded kleuren aan thematokens binden | Bob — open. Stand 2026-09-24 (twee verse exports na Bob's ronde): alleen ContainerWat/Organisatie/Entree op uitgaansevenementAanmaken staan op Alternate (door Claude). Bob's ronde van 2026-09-24 is NIET in de export geland — nog steeds 0xFFEEEEEE op uitgaansevenementAanmaken r1140 (ContainerWanneer) en 5× op stadsactiviteitAanmaken, en 0xFF09B34A op tagCategorieComponent én HorecagelegenheidoverzichtKaart. Vermoedelijk is de tekst/hex bewerkt i.p.v. het kleurblokje aangeklikt (dat levert opnieuw een hex op), of is een andere eigenschap (Fill i.p.v. Border) gezet. Eigen blok hieronder |
✅ AF (Bob 2026-09-20, export-geverifieerd): READ_CALENDAR/WRITE_CALENDAR én NSCalendarsUsageDescription zijn weg; CAMERA staat er nog (terecht) |
||
| 122 | Teksten aanmaakpagina's | ✅ A/B/C af (Claude 2026-09-20, 19 teksten, export-geverifieerd): verkeerde woorden, typefouten, technische veldnamen en het sterretje bij de categoriekeuze. Open blijven D (hints die het label herhalen), E-restje en F (Select... op een NL-pagina) — eigen blok hieronder |
| 123 | mijn_stadsrechten uitbreiden met de keten |
Code staat klaar en is op productie getest. Vervang regels 316-372 van custom.evenementen_aanmaken.inc + cc all. Blokkeert 124 |
| 124-126 | Stadsrechten-cascade op stadsactiviteitAanmaken |
Voorvullen van provincie/gemeente/plaats (124), een crash bij een lege plaatsenlijst (125) en een stille verkeerde plaats bij het wisselen van gemeente (126). Eigen blok hieronder |
| 128-A | ✅ KLAAR — nieuwsbrieven-resource staat live | Gedeployd en getest op productie 2026-09-21; de tikfout in de omschrijving van tid 36676 is ook weg (gemeten 2026-09-24) |
| 129 | Sessie-verlopen-melding afmaken/verifiëren | Niet in deze chat (Bob 2026-09-21). De afhandeling bestaat: drupalRequest herkent 401 en 403-met-"anonymous", zet sessieVerlopen en VerbindingsBanner hangt in HeaderButtonsComponent. Staat alleen in de builder, niet in de gecommitte lib/ — dus nooit geëxporteerd/gecommit. Nalopen of het af is en of de banner ook echt verschijnt. Eigen blok hieronder |
| 130 | Inloggen met Facebook/Google (OneAll) | Wens Bob 2026-09-21. Onderzocht en akkoord: OneAll-abonnement gaat naar Personal Advanced ($27/mnd jaarlijks) voor Direct Connect. Geen Play-blocker, maar wél de privacyverklaring/Data Safety als het meegaat in de eerste release. Volledig stappenplan in een eigen blok onderaan |
#
(P2-45b, P2-45c en 76 zijn op 2026-09-19 afgerond en geverifieerd met een verse export — zie de commit van die datum.)
⬜ De vier lege-lijst-teksten op mijnProfiel hebben geen Engelse
vertaling. Een component-parameterwaarde is een letterlijke string, geen
i18n-sleutel, dus er is geen globe. Bewuste keuze (2026-09-20); meepakken bij
de bredere vertaalronde (P1-17), niet los.
⬜ MijnAanmeldingen: de "Gepubliceerd"-tak is nooit met echte data gezien.
Alle testaanmeldingen op devbob staan op status: 0, dus de lijst toont
uitsluitend "Wacht op goedkeuring". De tekst komt kant-en-klaar uit Drupal
(status_label, een ternary op (int) $node->status === 1), dus het risico is
klein — één testnode publiceren en de lijst openen volstaat om dit af te
vinken.
⬜ Optioneel: ingang naar MijnAanmeldingen op Favorieten tab 4. Nu loopt
de enige route via MijnProfiel → Bekijk alles. Dat volstaat; alleen
oppakken als je 'm ook direct vanaf Favorieten wilt kunnen bereiken.
Open rest — vier punten, alle vier klein:
mijnProfiel
haalt in On Page Load zelf favorieten_gemeenten.json op (nieuwe API Call
FavorietenGemeenten, kopie van Nieuwsbrieven mét Cookie-header) naar de
page state favGemeenten; de rode regel hangt nu aan
!(_model.favGemeenten.isNotEmpty). Export-geverifieerd.tabIndex (Integer,
optioneel, default 0) op mijnProfiel, Initial Tab Index eraan gebonden,
en op Favorieten tab 2 onder de lijst een Text (NL+EN, padding 16, kleur
Secondary) met Navigate To mijnProfiel, tabIndex = 3. Export:
pushNamed(MijnProfielWidget.routeName, ... 'tabIndex': serializeParam(3.status == 2-regel uit 128-F (één conditionele Text); komt met
custom_nieuwsbrieven_confirm = FALSE nu nooit voor.Wens Bob 2026-09-21. Op de website kan een ingelogde gebruiker op
/user/<uid>/simplenews drie nieuwsbrieven aan- en uitvinken. Dat moet ook in
de app kunnen. Plan is af en volledig doorgemeten (2026-09-21); alleen A moet
gebouwd/gedeployd worden vóór B t/m D kunnen.
Simplenews 7.x-1.1, nieuwsbrieven zijn taxonomy-termen in vocabulary
newsletter (vid 11). ⚠️ variable_get('simplenews_vid') geeft 0 — leid de
vocabulary dus nooit daaruit af, gebruik simplenews_category_get_visible().
| tid | Nieuwsbrief | actief | onbevestigd | uitgeschreven |
|---|---|---|---|---|
| 18017 | Ondernemers nieuwsbrief | 11 | 360 | 0 |
| 36667 | Uitgaanskrant voor bezoekers van de horeca | 16 | 47 | 1 |
| 36676 | Uitgaanskrant.com wekelijkse uitgaansagenda | 18 | 43 | 2 |
Opslag: simplenews_subscriber (snid/mail/uid/activated) +
simplenews_subscription (snid/tid/status/source). Status: 1 = actief,
0 = uitgeschreven, 2 = onbevestigd.
De wekelijkse agenda (36676) hangt al aan de app. De edities heten
"Favoriete Gemeenten" en selecteren content via de flag favorite_town
(fid 2) — precies de flag die "Gemeente volgen" in
SelectStateDropDownComponent al zet via favorieten/flag. Er hoeft dus niets
gekoppeld te worden; alleen het abonnement zelf ontbreekt.
simplenews_subscribe_user($mail, $tid, $confirm, $source) heeft twee takken
(simplenews.module:1281):
$confirm = FALSE → status meteen 1 (actief), géén mail, alleen
module_invoke_all('simplenews_subscribe_user', ...). Dit is wat de
user-settings-pagina uit het screenshot doet
(simplenews.subscription.inc:97, source 'website').$confirm = TRUE → status 2 (onbevestigd) + bevestigingsmail met
link; pas ná het klikken wordt het actief. Hier komen die 360 onbevestigde
inschrijvingen vandaan.Een welkomstmail bestaat niet: simplenews_rules levert alleen het event,
en geen van de 6 Rules op de site luistert ernaar (alle zes hangen aan
node_insert/user_insert/node_presave). "Drupal doet dat standaard al"
klopt dus niet voor de ingelogde route.
Opgehelderd 2026-09-21 (Bob, met de simplenews-instellingenpagina erbij): de app stuurt GEEN bevestigingsmail, en dat is correct. De drie nieuwsbrieven staan op opt-in/out-methode Double, en simplenews omschrijft die stand zelf als: "anonymous users receive an (un)subscription confirmation email. Authenticated users are (un)subscribed immediately." Een app-gebruiker is per definitie ingelogd, dus die hoort direct verwerkt te worden.
⚠️ Maar simplenews past die regel niet zelf toe. simplenews_subscribe_user()
bevat geen enkele check op de ingelogde gebruiker — hij doet blind wat de
$confirm-parameter zegt (gecontroleerd in de broncode). De aanroeper bepaalt
die waarde, en het websiteformulier doet dat met
simplenews_require_double_opt_in($tid, $account): FALSE zodra het
mailadres van de ingelogde gebruiker zelf is, anders de opt-in-methode van de
nieuwsbrief. De resource roept sinds 2026-09-21 precies diezelfde functie aan,
dus de app volgt de website automatisch — ook als de opt-in-instelling ooit
wijzigt. Read-only geverifieerd op productie: voor de ingelogde gebruiker geeft
hij false op alle drie de nieuwsbrieven, voor een vreemd adres true.
De variabele custom_nieuwsbrieven_confirm blijft bestaan als noodrem (niet
gezet = automatisch, 1 = altijd mail, 0 = nooit), maar is normaal niet nodig.
Uitschrijven gaat altijd direct, ook als bevestiging aan zou staan: anders drukt de gebruiker in de app op "uit" en blijft hij abonnee tot hij een mail opent.
De resource geeft toch de STATUS terug, niet een kale bool (1 aan / 2 wacht op bevestiging / 0 uit). Dat kost niets en houdt de deur open.
Van 450 anonieme inschrijvingen is er in tien jaar geen enkele bevestigd, terwijl alle 45 account-inschrijvingen meteen op actief staan:
| anoniem (uid 0) | met account | |
|---|---|---|
| status 1 | 0 | 45 |
| status 0 | 0 | 3 |
| status 2 | 450 | 0 |
Dat leek op een kapotte mailroute, maar het is exact de tweedeling die de Double-stand voorschrijft. Raakt de app niet. Wat blijft staan is een lage conversie op de publieke website-formulieren (0 % bevestigd) — ooit misschien een eigen kijkje waard, geen taak.
nieuwsbrieven · Eigenaar: Bob✅ 128-A IS KLAAR — gedeployd en end-to-end getest op productie (Bob,
2026-09-21). nieuwsbrieven.json levert de juiste lijst (zonder de rol
Horeca-owner zie je 2 van de 3), en subscribe.json gaf
{"tid":"36667",…,"status":1,"geabonneerd":true} — direct actief, geen
bevestigingsmail, precies zoals bedoeld. custom_nieuwsbrieven_confirm staat op
FALSE.
Zonder X-CSRF-Token antwoordt het endpoint met ["CSRF validation failed"]
(gemeten 2026-09-21; mijn eerdere inschatting dat de token niet afgedwongen werd,
was fout). Met de token erbij werkt het meteen.
De app is hier al op ingericht, mits je het bestaande patroon volgt:
drupalRequest zet de header zodra het 5e argument gevuld is, en
drupalLogin schrijft de token bij het inloggen naar FFAppState().userToken
(lib/custom_code/actions/drupal_login.dart:45). Geef bij de subscribe- en
unsubscribe-actie dus exact dezelfde vijf argumenten mee als het
favorieten-hartje op HorecagelegenheidCurrent:
userSessionname, userSessionid, userToken, plus de body.
Vergeet je die token, dan faalt het STIL. drupalRequest geeft bij elke
non-2xx gewoon [] terug (regel 143), dus er komt geen foutmelding: de switch
lijkt te werken en er gebeurt niets. Controleer een nieuwe actie daarom met
adb logcat | grep "I flutter" — die action print STATUS en de body.
unsubscribe.json is nog niet één keer aangeroepen. Zelfde route als
subscribe, dus laag risico; de app-test dekt het vanzelf af.source = app in {simplenews_subscription} is voortaan via de
app binnengekomen — gratis telling, stond op 0 vóór de eerste test._custom_nieuwsbrieven_zichtbaar() is de spil en wordt door alle drie de
callbacks gebruikt: hij geeft simplenews_category_get_visible() terug (dat
laat hidden-nieuwsbrieven automatisch weg), minus tid 18017 wanneer de
gebruiker de rol Horeca-owner (rid 5) niet heeft — Bob 2026-09-21: selecteer
op rol, niet op het bezit van een horecagelegenheid. Er zijn 11 accounts met die
rol, waarvan er nu 1 op de ondernemersnieuwsbrief zit.
Door die filtering server-side te doen heeft de app géén conditie nodig, en
kan een gemanipuleerd verzoek zich er ook niet op abonneren — mits
subscribe/unsubscribe de binnenkomende tid tegen diezelfde lijst controleren
(if (!isset($lijst[$tid])) return services_error('Onbekende nieuwsbrief', 400);).
Zonder die guard kan een client zich op een verborgen nieuwsbrief abonneren.
Subscribe/unsubscribe zijn daarna drie regels elk, met
simplenews_subscribe_user($user->mail, $tid, FALSE, 'app'). Gebruik source
'app' (niet 'website'), dan is later meetbaar hoeveel abonnees uit de app
komen. Geef als respons dezelfde rij terug als in de index, zodat de app de
nieuwe status meteen kan tonen.
⚠️ Resource aanzetten is een APARTE stap na het deployen: in
/admin/structure/services/list/flutterdrup/resources de regel nieuwsbrieven
aanvinken én de drie losse operaties eronder — precies waar
mijn_aanmeldingen maandenlang op 404 stond. Daarna cc all.
Verificatie: anoniem curl moet 403 geven (route bestaat, sessie
ontbreekt), niet 404.
⚠️ Nooit edge-cachen — user-specifiek. Het nieuwe pad valt buiten de
bestaande Cloudflare Cache Rules (die matchen op /flutterdrup/views/…,
plaatsen), dus dat gaat vanzelf goed; het mag er alleen nooit bij.
Staat live op mijnProfiel, achter Account, getest op een toestel met
bobcity ingelogd: lijst laadt, aan- en uitzetten geeft 200 en de rij springt
meteen om. Opbouw zoals gebouwd:
Nieuwsbrieven (GET, Cookie: '${sessionName}=${sessid}'),
Predefined Path items = $[:] (Is List) → null-veilige
NieuwsbrievenCall.items(...)?.toList() ?? [].Column > ListView met Backend Query + Generate Dynamic Children.Row (padding 16/10/16/10) met Column > [Text $.naam
(Title Small), Text $.omschrijving (Body Small, secondaryText)] en rechts
twee conditionele IconButtons: toggle_off (grijs) → subscribe,
toggle_on (Primary) → unsubscribe. Conditie = custom function
nieuwsbriefAan(item), met/zonder Apply Opposite Statement.drupalRequest('POST', '…/nieuwsbrieven/(un)subscribe.json', …,
FFAppState().userToken, '{"tid": <$.tid>}') + Rebuild Page.⚠️ De omschrijving van tid 36667 is in Drupal gelijk aan de naam ("Uitgaanskrant voor bezoekers van de horeca" staat er twee keer onder elkaar). Geen app-bug — op te lossen in de term-beschrijving. Eigenaar: Bob.
Staat onder de lijst: een rode regel "Je volgt nog geen gemeenten, dus de
wekelijkse agenda blijft leeg. Tik hier om gemeenten te kiezen." achter
if (!(FFAppState().favorieteGemeenteIds.isNotEmpty)), met een Navigate To
naar selectprovinciegemeente. Daaronder staat altijd de oranje regel "De
wekelijkse uitgaansagenda gebruikt de gemeenten die je volgt…" met dezelfde
navigatie. Beide vertaald (NL+EN).
⚠️ Bekende beperking: favorieteGemeenteIds wordt alléén gevuld door
SelectStateDropDownComponent (die haalt in zijn On Page Load
favorieten_gemeenten.json op en schrijft de nids naar App State). Het veld is
persisted, dus in normaal gebruik klopt het — maar wie zijn gemeenten op de
website of op een ander toestel heeft gekozen en het gemeente-scherm in
deze app nog nooit opende, ziet de waarschuwing ten onrechte. Live gezien op
emulator-5554 met bobcity: warning stond er, en verdween zodra het
gemeente-scherm één keer geopend was.
Wat de fix zou zijn (uitgezocht, niet gebouwd): op mijnProfiel in de
On Page Load hetzelfde lijstje ophalen. Dat kan niet met drupalRequest:
die custom action geeft dynamic terug en FlutterFlow biedt zo'n output niet
aan als bron voor een getypt veld — getest op zowel een List<String>
App-State-veld als een List<Json> page state, in beide gevallen rendert de
bronnenlijst leeg. Twee werkende routes:
FavorietenGemeenten (GET + Cookie-header, net als
Nieuwsbrieven) → Update Page State favGemeenten met Action Outputs →
JSON Body → No Further Changes; conditie dan op die page state; offavorieteGemeenteNids een non-nullable return geven (List<String> in
plaats van List<String>?), waarna hij wél als bron verschijnt voor
Update App State → favorieteGemeenteIds → Set Value. Dat haalt meteen de
! weg uit SelectStateDropDownComponent.Het page-state-veld favGemeenten (List<Json>) staat al klaar op
mijnProfiel — aangemaakt voor route 1, nu nog ongebruikt.
Wie schrijft dat veld? Precies één plek (gemeten op een verse export):
SelectStateDropDownComponent (+ zijn twee dode kopieën) vult
FFAppState().favorieteGemeenteIds in zijn On Page Load. Favorieten tab 2
doet dat NIET — die leest zijn lijst rechtstreeks van de server. Wie dus
alleen Favorieten opent en nooit het gemeente-keuzescherm, houdt een leeg veld.
Dat maakt 128-E een mooie kans: als de tekst daar tóch komt, kan diezelfde tab
meteen ook App State bijwerken.
Alles wat in de tab staat is NL+EN ingevuld via het globe-icoontje, en de
5 EN-vertalingen die bij de tab-verhuizing gewist waren zijn hersteld:
Wachtwoord wijzigen/Account verwijderen/Uitloggen en de twee
Kopie-knoppen (die kregen bij de hernummering nieuwe sleutels,
oxse6aci en wcyy4pao — de sleutels die hier eerder genoteerd stonden
zitten in de dode kopie mijn_profiel_copy4).
Nog niet gebouwd: de regel bij status == 2 ("Check je mail om je
inschrijving te bevestigen"). Die stand komt met custom_nieuwsbrieven_confirm
= FALSE nooit voor; de resource geeft de status wel al terug, dus het is later
één conditionele Text erbij.
⚠️ De omschrijvingen van de nieuwsbrieven komen uit Drupal en krijgen dus géén globe en geen EN-vertaling. Dat is een bewuste beperking.
450 onbevestigde inschrijvingen (360 + 47 + 43). Die mensen hebben zich ooit
aangemeld maar nooit op de bevestigingslink geklikt en krijgen dus niets. Geen
app-probleem, wel het sterkste argument voor confirm = FALSE in de app.
Volledige doorloop op emulator-5556 (telefoon) en emulator-5554 (tablet)
met een verse release-APK uit een eigen export, zowel uitgelogd als ingelogd als
bobcity (Bob logde de telefoon halverwege in).
dart analyze gaf 0 errors; er is geen enkele exception of overflow in
logcat gevallen. Offline-gedrag, netwerkherstel, zoeken, datumfilters,
categoriefilter, login-foutafhandeling en de nieuwsbrief-toggles werkten alle
correct. Hieronder wat wél mis is, op volgorde van ernst.
Bob 2026-09-22: "als iemand stadsrechten heeft, mag de activiteit direct gepubliceerd worden. Alleen gewone gebruikers die een activiteit aanmaken, die moeten eerst in drupal worden goedgekeurd." De app-tekst klopt dus; de backend doet het niet.
Huidige stand: _custom_stadsactiviteit_create() zet onvoorwaardelijk
$node->status = 0 en neemt bewust géén status-argument aan, en de docblock
noemt mijn_stadsrechten letterlijk "PUUR INFORMATIEF … GEEN toegangscontrole".
Iedere app-inzending wacht daardoor op de redactie, ook die van bobcity (die
stadsrechten heeft op Amsterdam, Drechterland, Enkhuizen en Stede Broec).
Wat er moet gebeuren, en waarom het niet triviaal is:
status uit
de client, anders kan iedereen publiceren.field_town_access van de gebruiker met de gekozen plaats van de
activiteit. Let op het niveau-probleem uit CLAUDE.md: dat veld kan een
provincie-, gemeente- óf plaats-tid bevatten (bobcity heeft nu gemeenten,
eerder stonden er plaatsen en provincies in). Bepaal het niveau met
count(taxonomy_get_parents_all($tid)) → 1 provincie / 2 gemeente / 3 plaats,
en kijk of de gekozen plaats onder een van de rechten-tids valt.status = 1, anders status = 0.~/tmp/132c-stadsrechten-publiceren.txt (Claude 2026-09-24) — vervangt
de twee regels $node->status = 0; $node->promote = 0; in
_custom_stadsactiviteit_create(), gebruikt de al aanwezige $ancestors,
geen nieuw argument. Met testplan (devbob eerst, drie gevallen) en terugrol.Pas ná deze wijziging klopt de introtekst (rupw26n4) met de werkelijkheid. De
tekst zelf hoeft dus niet aangepast; wel controleren zodra de backend om is.
Agenda-knop op EventCurrent heeft nu dezelfde rechterpadding (12) als de
deelknop, de categorie-Wrap eronder rechts 12 i.p.v. 1, en het
favorietenhartje op HorecagelegenheidCurrent (de Tooltip-node) rechts 12.
Bijvangst, geen taak: elk categorielabel op EventCurrent zit nog in een
Align(1.0, 0.0), waardoor de labels onder elkaar rechts uitgelijnd staan i.p.v.
naast elkaar te stromen — hetzelfde patroon als P2-34/P1-41. Alleen aanpassen
als Bob ze liever naast elkaar wil (Alignment ↺ + Child Alignment ↺ op de
label-container).
Op een Home-kaart met 4 labels (die op 2 regels vallen) wordt de omschrijving
zonder "…" afgekapt — zie Ondernemend Dijk en Waard op de tab Cultuur & Info.
De labels eten de ruimte op die de responsive Max Lines veronderstelt. Bij één
label klopt het wel. Overweeg de labels op één regel te begrenzen, of de
omschrijving één regel korter.
Het component LegeLijstMelding (lib/shared/lege_lijst_melding/) vervangt
overal het kale logo800px.png. Het is tweetalig via de custom function
tweetalig(nlText, enText) — zie CLAUDE.md voor dat patroon.
Stand (geverifieerd met een verse export, dart analyze 0 errors):
15 instanties, alle met NL én EN gevuld — Home-tabs, beide sliders,
PUitgaanPage, horecagelegenhedenOverzichtCurrent (6 tabs), thuisBezorgen,
MijnAanmeldingen, Favorieten (3 tabs). NL- en EN-tak zijn op een release-APK
bewezen.
1 · HorecagelegenhedenOverzichtProvinciePage — 5 van de 6 tabs · Eigenaar: Bob
Stand 2026-09-24 (export-geverifieerd): tab 4 (Overnachten) staat al — Claude
zette 'm op 2026-09-23, met de lange variant "Geen overnachtingen gevonden.
Probeer een andere categorie." / "No places to stay found. Try another
category." (wil je de korte variant uit de tabel hieronder, pas dan alleen die
twee Value-velden aan). Tab 1, 2, 3, 5 en 6 staan nog op logo800px. Claude
kwam er via browser-automation niet verder: de gefilterde boom toonde maar drie
van de zes tabs en scrolt niet (zie CLAUDE.md). Exacte stappen, per tab:
StaggeredView → zes rijen in tabvolgorde (1 Activiteiten ·
2 Cultuur · 3 Eetgelegenheden · 4 Overnachten ✅ · 5 Uitgaan · 6 Verhuur).
Klik de rij.Widget Type → dropdown staat op
Image → kies Component.Component → knop Select Component → zoek
LegeLijstMelding → aanklikken. Center Component staat dan al aan.Parameter → rij nlTekst openklappen → Value → NL-tekst uit
de tabel hieronder → rij enTekst → Value → EN-tekst. (De toggle "Show
Empty List Widget" staat op deze pagina al aan; niets aan doen.)grep -c logo800px lib/horecagelegenhedenoverzicht/horecagelegenheden_overzicht_provincie_page/*_widget.dart
hoort 0 te geven, en grep -n "nlTekst:" -A1 <zelfde bestand> toont de
zes teksten in tabvolgorde.| Tab | nlTekst | enTekst |
|---|---|---|
| 1 Activiteiten | Geen activiteiten gevonden. Probeer een andere categorie. | No activities found. Try another category. |
| 2 Cultuur | Geen culturele gelegenheden gevonden... | No cultural venues found... |
| 3 Eetgelegenheden | Geen gelegenheden gevonden... | No places found... |
| 4 Overnachten | Geen gelegenheden gevonden... | No places found... |
| 5 Uitgaan | Geen gelegenheden gevonden... | No places found... |
| 6 Verhuur, catering | Geen gelegenheden gevonden... | No places found... |
2 · mijnProfiel, tab Nieuwsbrieven — die ListView (op NieuwsbrievenCall)
heeft géén lege staat. Voorstel: "Er zijn op dit moment geen nieuwsbrieven." /
"There are no newsletters at the moment."
3 · Drie FOTOcarousels — eigen besluit, geen lijsten. Een tekstmelding in een
beeldcarousel is discutabel: HorecagelegenheidCurrent (toont nu een logo),
EvenementHorecagelegenheid en EventCurrent (tonen niets).
4 · Optioneel: 4× het oude LegeLijstComponent op mijnProfiel (+1 in
FotoboekGridComponent) omzetten naar LegeLijstMelding. Werkt prima, alleen
eentalig — geen bug.
Wezen, niet doen: evenement_component (pagina Event, 0 aanroepers) en
SliderUitgaanComponentSmallCurrent (0 aanroepers).
⚠️ Kleine inconsistentie, geen bug: Center Component staat op de horeca-tabs
uit en op MijnAanmeldingen aan.
MijnAanmeldingen toont UITGELOGD vier kaarten met "null" · Eigenaar: Bob (plan door Claude, 2026-09-24)Exacte stappen (geen Drupal, geen kaart-wijziging):
MijnAanmeldingen → Widget Tree → filter StaggeredView → de lijst-rij →
rechterpaneel Visibility → Conditional aan → klik het rode Unset →
zoek userSessionid → App State → userSessionid → Available Options
Is Set and Not Empty → Confirm. Export hoort dan
if (FFAppState().userSessionid != null && FFAppState().userSessionid != '')
om de lijst te geven; uitgelogd wordt de lijst niet gebouwd en vertrekt de
call ook niet.Expanded) in zit: "+" op de
Column-rij → 2e tab-icoon (Project Components) → LegeLijstMelding →
parameters nlTekst = Log in om je aanmeldingen te zien. / enTekst =
Log in to see your submissions. → op dit component Visibility →
Conditional aan → zelfde bron userSessionid → Is Set and Not Empty →
toggle Apply Opposite Statement aan → Confirm.grep -c "userSessionid != ''" lib/mijn_aanmeldingen/mijn_aanmeldingen_widget.dart
hoort 2 te geven (één positief, één met !).Gevonden 2026-09-23 op een release-APK na pm clear. Het endpoint geeft anoniem
een foutmelding-array terug, en die wordt als lijst-met-items gerenderd: vier
witte kaarten met 3× null en een Kopie-knop. Hoort een lege staat of een
inlog-uitnodiging te zijn. (Terzijde: hierdoor werkt "uitgelogd" niet als test
voor een lege lijst — zet in plaats daarvan het netwerk uit, zie CLAUDE.md.)
Stand 2026-09-23: tab 1-3 tonen inmiddels een LegeLijstMelding ("Je hebt nog geen…"), dus geen leeg scherm meer — maar uitgelogd is die tekst misleidend.
Exacte stappen (3 lijsten × 2 parameters = 6 bindingen, allemaal hetzelfde):
De teksten zijn parameters op de LegeLijstMelding in de Empty List Widget van
elke lijst; die worden een Conditional Value op "ben ik ingelogd?".
StaggeredView (tab 1 en 3) resp.
ListView (tab 2) → de lijst-rij aanklikken.Parameter → rij nlTekst openklappen → klik het
kleine ⚏-icoontje naast het label Value (niet in het veld) →
Set-from-Variable-dialoog → zoek Conditional → klik de bronregel
Conditional Value (If/Then/Else) zelf (de rode "No Available Options"
eronder is misleidend).userSessionid → App State → userSessionid →
Available Options Is Set and Not Empty → Confirm.
THEN: de huidige tekst (staat in de tabel). ELSE: de inlogtekst.
→ Confirm.| Tab | THEN (nu al) | ELSE nl / en |
|---|---|---|
| 1 Persoonlijke agenda | Je persoonlijke agenda is nog leeg. / Your personal agenda is still empty. | Log in om je persoonlijke agenda te zien. / Log in to see your personal agenda. |
| 2 Favoriete gemeenten | Je volgt nog geen gemeenten. / You are not following any municipalities yet. | Log in om je favoriete gemeenten te zien. / Log in to see your favourite municipalities. |
| 3 Favoriete gelegenheden | Je hebt nog geen favoriete gelegenheden. Tik op het hartje… / You have no favourite venues yet… | Log in om je favoriete gelegenheden te zien. / Log in to see your favourite venues. |
Controle: grep -n "userSessionid != ''" -A2 lib/favorieten/favorieten_widget.dart
hoort 6 ternaries met ? '…' : 'Log in …' te geven.
Tab 1-3 tonen niets en geen uitleg; de uitnodiging "Met een account bewaar je
favoriete gemeenten…" staat op tab 4, twee tabs naar rechts. Verwijs vanaf de
eerste tab naar die knop (of toon LegeLijstComponent met die tekst).
zoekActief wordt nergens gelezen of gezet; de
component-parameter parameter1 van FilterBalkComponent wordt nergens
in dat component gebruikt (Home geeft er _model.zoekOpen aan mee,
PUitgaanPage letterlijk false); en de page state zoekOpen op Home
wordt nergens meer geschreven — dat is een restant van vóór de verhuizing naar
App State.functions.buildStempel()
staat onderaan de drawer én op Favorieten en toont onbekend zolang er
geen --dart-define=BUILD_TS=… meegaat — en FlutterFlow's eigen deploy geeft
die nooit mee, dus in de winkelversie staat er gegarandeerd "onbekend".
Twee opties (Claude 2026-09-24): (a) de Text op beide plekken weghalen;
(b) vervangen door het échte versienummer: een custom widget VersieLabel
met dependency package_info_plus dat PackageInfo.fromPlatform() leest en
v{version} ({buildNumber}) toont — dat is precies het nummer dat
FlutterFlow bij elke deploy ophoogt (App Settings → Mobile Deployment),
dus het loopt vanzelf mee. buildStempel() + BUILD_TS kunnen dan weg.
Advies: (b), ~20 regels, Claude kan dit bouwen.MijnProfiel staan ruw (2026-09-22 06:06:00) terwijl de rest
van de app dinsdag 22 sep, 14:00 gebruikt. Geldt voor Mijn evenementen en
Eigen activiteiten.favorieten_widget.dart:435 geeft hardcoded inhoud: null mee. Nagemeten
2026-09-22 (respons uit logcat van de ingelogde app):
favorieten_agenda.json heeft géén omschrijvingsveld. Volledige veldenset:
node_type, nid, titel, datum, datum_raw, plaats, categorie, logo, adres,
postcode, woonplaats, horecagelegenheid, horecagelegenheidNid, matched_via.
De binding is dus niet "vergeten" — er valt niets te binden. Keuze voor Bob:
óf een omschrijvingsveld aan de resource toevoegen (Drupal), óf de
placeholderregel weghalen zodat de kaart gewoon zonder tekst staat.
(Terzijde: horecagelegenheidNid mét hoofdletter N is voor dít endpoint
correct — getest, het horeca-blok op de detailpagina komt gewoon door. Niet
"gelijktrekken" met de $.horecagelegenheidnid van de views.)PUitgaanPage en het
horeca-overzicht); op het horeca-overzicht legt de 468×60-banner zich over de
onderste kaart heen. Zie ook G0.6.Offline starten (rode banner Geen verbinding met uitgaanskrant.com + werkende
knop Opnieuw, geen crash, geen grijs blok) · netwerkherstel · de vier
datumfilters (nagemeten tegen de API) · zoeken op titel · de tab-bewuste
categoriedropdown · login-foutafhandeling · de nieuwsbrief-toggles, die per rij
onafhankelijk schakelen en netjes subscribe/unsubscribe met CSRF-token
sturen (status 200) · EventCurrent inclusief het horeca-blok, ook als je er via
de persoonlijke agenda komt · de responsive 1/2/3-kolommen op tablet.
Bob 2026-09-21: niet in deze chat oppakken, alleen noteren.
Het bestaat wél — ik beweerde eerst van niet, op basis van de gecommitte
lib/, en dat was te snel. Een verse export van 2026-09-21 toont:
lib/custom_code/actions/drupal_request.dart herkent 401, en 403 mét
het woord "anonymous", maar alleen als er daadwerkelijk een cookie meeging
(een 403 zonder dat woord is een rechtenkwestie en mag niemand uitloggen);
hij zet dan FFAppState().sessieVerlopen = true.VerbindingsBanner staat op header_buttons_component_widget.dart:275, dus
in de gedeelde header.Wat er te verifiëren valt: of de banner in de praktijk ook echt verschijnt
bij een verlopen sessie, en of de overzichten dan niet leeg blijven (dat was de
aanleiding). Reproduceren kan door userSessionid in App State te vervuilen en
een pagina te openen die een drupalRequest doet (Favorieten, Mijn Profiel).
⚠️ Dit werk staat alleen in de builder, niet in git. Zie de waarschuwing hieronder.
lib/ en de builderGemeten 2026-09-21 met een verse export: diff -rq lib/ /tmp/ff-nb/lib/ geeft
86 regels. Daar zit onder meer in: de complete sessie-verlopen-afhandeling,
VerbindingsBanner in de header, de FilterBalkComponent-familie, een vierde
tab-indeling op mijnProfiel (Mijn evenementen / Stadseditor / Account,
waar de gecommitte versie nog losse blokken zonder tabs heeft) en
crashlytics_test.dart.
Niet gecommit door mij, want er liep op dat moment een andere FlutterFlow-sessie en dat werk kan half af zijn (staande regel: niet over andermans werk heen committen). Zodra die sessie klaar is, is een verse export
Ideeën die af zijn onderzocht maar bewust wachten. Bob 2026-09-20: eerst live, dan pas kijken of dit überhaupt de moeite is.
Bob's vraag: "ik kan een lijstje domeinen opgeven — maar hoe weet mijn app wat de URL is? een evenement, een horecagelegenheid, een stadspagina?"
Antwoord: Android geeft de VOLLEDIGE URL aan de app, en de app moet die zelf vertalen naar een route. Dat vertalen is het hele werk; het domeinlijstje bepaalt alleen of de app aan de beurt komt.
Daarom NIET uitgaanskrant.com registreren. Twee redenen:
/nl/Noord-Holland/Amsterdam/Bimhuis-Amsterdam). De app kan daar niets mee
zonder Drupal om een vertaling te vragen — een extra endpoint erbij.Wel uitgk.com. Dat is jouw eigen korte domein, het wordt alleen gebruikt door
de deelknop (event_current_widget.dart:414) en door zetInAgenda
(zet_in_agenda.dart:42), en het heeft één voorspelbare vorm: /<nid>.
Het type is met één API-call te bepalen — gemeten 2026-09-20:
flutterflow_events.json?nid= |
flutterflowmobiel_establishment_info.json?nid= |
|
|---|---|---|
| 222338 (event) | 1 rij, mét horecagelegenheidnid=75459 |
0 rijen |
| 75459 (zaak) | 0 rijen | 1 rij |
Dus: één call naar flutterflow_events.json?nid=X; komt er een rij uit, dan is het
een event en heb je de horecaid meteen te pakken (die heeft EventCurrent
als tweede parameter nodig). Nul rijen → behandel als zaak →
HorecagelegenheidCurrent(nid: X). Allebei leeg → Home.
Stadspagina's erbuiten laten. PUitgaanPage wil een plaats-tid, en die staat
in geen enkele URL — dat zou een derde lookup via plaatsen.json vergen voor
weinig winst.
Twee routes om uit te kiezen:
uitgk.com/222338) + een routerpagina in de app.
Mooiste links. Vergt een pagina met een pad-parameter op rootniveau (/:nid),
de API-call hierboven en een conditionele navigatie. ⚠️ Open vraag: kan
FlutterFlow een routePath /:nid op rootniveau aan zonder te botsen met /home,
/favorieten enz.? go_router matcht exacte paden eerst, dus het zou moeten
kunnen — maar dat is niet gemeten. Eén testpagina + een export geeft uitsluitsel.uitgk.com/eventCurrent?nid=..&horecaid=... Dan matcht
go_router het pad meteen en hoeft er in de app niets gebouwd te worden (het
custom scheme werkt al precies zo). Prijs: lange, lelijke deellinks, plus een
redirect-regel op uitgk.com voor wie de app niet heeft.Bob 2026-09-20: naar 2.0. "niet belangrijk voor de livegang, als het überhaupt belangrijk is." Terecht — de deelknop werkt al, hij opent alleen de website in plaats van de app. Pak dit pas op als blijkt dat mensen gedeelde links vaak volgen.
Volgorde-afhankelijkheid, en dus hoe dan ook na de launch:
/.well-known/assetlinks.json op uitgk.com moet de SHA-256 van het
Play-app-signing-certificaat bevatten, en dat certificaat krijg je pas ná de
eerste upload (Play Console → Setup → App integrity). Vóór die tijd is de
verificatie niet af te maken.
Elke tab-tik doet een verse fetch, ook als je terugkeert naar een tab die je al
bezocht hebt; heen-en-weer klikken tussen zes tabs haalt dus zes keer opnieuw op.
Een guard ("sla de fetch over als alleXxx al gevuld is") lost dat op, maar kost
een conditie in vijf actieketens — en juist die dialoog is op deze pagina fragiel.
Alleen doen als het in de praktijk hindert.
Bob 2026-09-23: mensen moeten hun geüploade foto kunnen bewerken — tekst erop,
croppen, filters. Onderzocht op 2026-09-23; besluit: pro_image_editor, geen
betaalde dienst.
pro_image_editor (pub.dev, BSD-3-Clause, gratis ook commercieel) doet in
pure Dart precies wat gevraagd is: tekstlagen (font, kleur, achtergrond,
uitlijning), croppen/roteren/spiegelen, tekenen, stickers/emoji, filters, blur,
lagen slepen/schalen/draaien, undo/redo. Android + iOS + web. In: Uint8List,
uit: Uint8List. Gezond onderhouden: 592 likes, 160/160 pub points, ±56k
downloads/maand, laatste release 6 dagen vóór de meting.
De betaalde alternatieven zijn afgevallen op architectuur, niet op prijs:
| Dienst | Prijs | Waarom niet |
|---|---|---|
| img.ly PhotoEditor SDK | ±$1.000-3.000/jaar | Mooiste UI, maar overkill voor tekst-op-foto |
| Uploadcare | gratis tier, daarna ~$100+/mnd | Wil zélf het bestand hosten |
| Filestack | vanaf ~$70/mnd | Idem |
| Cloudinary | gratis tier | Transformaties via URL, géén gebruikerseditor |
Die laatste drie zetten hun eigen storage naast Drupal. Wij hebben een werkende
keten bestandUpload → file_managed → fotoboek, inclusief het
eigenaarschapsmodel (403 op andermans fids, zie de fotoboek-notitie in
CLAUDE.md). Een externe uploaddienst ertussen breekt dat of dupliceert het.
Webview-route (Pintura, tui.image-editor) ook afgevallen: bytes heen en weer
via een JS-bridge, en dat patroon heeft in dit project al eerder pijn opgeleverd.
Gemeten 2026-09-23 tegen de pub.dev-API:
sdk >=3.12.0 + flutter >=3.44.0;12.0.0 (2026-02-13, sdk >=3.2.2,
flutter >=3.35.0) — de eerstvolgende, 13.1.0, springt al naar Dart 3.12.Exact hetzelfde patroon als bij add_2_calendar (zie CLAUDE.md): pub get
weigert dan met een misleidende "Try using the Flutter SDK version 3.4x". Zet dus
pro_image_editor: 12.0.0 — zonder caret, anders trekt pub bij de eerste
pub get alsnog 13+ binnen.
Wat je mist door te pinnen: de changelog van 13.x/14.x gaat vrijwel volledig over de tune-editor (sliders, stacking-bug) en stijl-opties. Niets aan de tekstfunctie, dus voor dit doel kost het pinnen niets.
12.0.0 vraagt drie packages iets hoger dan pubspec.lock nu vastheeft. Alle drie
bestaan in een versie die op Dart 3.9.2 draait, dus dit blokkeert niet:
| package | project nu | 12.0.0 wil | laagste die voldoet | Dart-eis daarvan |
|---|---|---|---|---|
http |
1.4.0 | ^1.6.0 |
1.6.0 | ^3.4.0 ✅ |
shared_preferences |
2.5.3 | ^2.5.4 |
2.5.4 | ✅ (t/m 2.5.5) |
web |
1.1.0 | ^1.1.1 |
1.1.1 | ^3.4.0 ✅ |
vector_math (2.2.0) en plugin_platform_interface (2.1.8) staan al goed.
Nog niet gemeten: of een ánder package in dit project http onder 1.6.0
vastzet. Dat blijkt bij de eerste pub get in een losse exportmap.
Dart-pakket, dus Custom Widget of Custom Action in lib/custom_code/, met
pro_image_editor: 12.0.0 toegevoegd via de Pubspec-dependencies in de builder.
De keten wordt: foto kiezen (bestaande knop) → editor openen → bewerkte bytes
terug → bestandUpload. Dat laatste stuk draait al.
Aandachtspunten die nu al bekend zijn:
selectMediaWithSourceBottomSheet is gegenereerde helper-code, zie
CLAUDE.md). De editor komt dus als losse stap ná het kiezen — sluit aan bij
hoe de Fotoboek/Mobiel-keuze nu al werkt (commit e5f46e7).bestandUpload stuurt
base64 in een JSON-body, dus dat past — maar let op post_max_size: een
bewerkte foto kan groter uitvallen dan het origineel. Overweeg Max
Width/Height op de mediakiezer.file_managed-rij (of het origineel
overschrijven, wat file_usage van andere nodes raakt). Buiten scope tenzij
Bob het expliciet wil.pub get in een losse exportmap — resolvet 12.0.0 met de drie bumps, en
geeft dart analyze 0 errors? Browserloos, ±10 minuten, wijzigt niets aan het
project. (Recept: exportmap + pubspec.lock + fvm use 3.35.7 -f, zie
CLAUDE.md.)ff-apk.sh vóór en ná.localizationsDelegates-regel als de app nog de SDK-MaterialApp gebruikt.
Nakijken of de Nederlandse labels via zijn eigen i18n-configuratie gaan (dus
níét via de globe uit internationalization.dart).*Opgesteld 2026-09-19. Account aangemaakt 2026-09-18 als Organisatie (Digital Force); wacht op verificatie. Vervangt taak 63 (a-i) en punt 2 van "Bob's 7" — die twee niet meer los afwerken. Vink hier af.*
Bevestigd via Google's eigen documentatie, 2026-09-19:
#
flutter_launcher_icons, en een crash op de splash zonder
--include-assets), plus een keystore die er nog niet is
(android/app/build.gradle:80 staat op signingConfigs.debug).
Sub-route: download het AAB in FlutterFlow en upload het handmatig in
Play Console. De directe "publish to Play"-koppeling vergt een Google
Play service-account-JSON; pas doen als handmatig uploaden gaat vervelen.
⚠️ Blijft staan als G4.3: controleer het icoon op het AAB dat je uploadt.~/uk-play-assets/play-teksten.md (gered uit een
vluchtige sessie-scratchpad, 2026-09-19 — stond nergens duurzaam).
⏳ Kan pas ingevoerd worden ná G1.1 (de app moet bestaan in Play Console).--release-build (export mét --include-assets,
flutter_launcher_icons vooraf, x64 voor de AVD's), uitgelogd (pm clear),
Nederlandse app-locale: 6 telefoon (1080x1731), 2x 7" tablet (1200x1812, AVD
tijdelijk op wm size 1200x1920 / density 240), 2x 10" tablet (2560x1456).
Status- én navigatiebalk weggecropt; alle ratio's onder 2:1.
📁 ~/uk-play-assets/screenshots/ + play-screenshots.zip; de oude set staat
in screenshots-oud-2026-09-19-middag/ (Claude gooit niets weg).
Opgelost t.o.v. de eerste set: header toont Heel Nederland i.p.v.
"Amsterdam (gemeente)"; de twee 10"-shots zijn nu echt verschillend (Home +
menu, md5 gecheckt); tel-05-zaak.png is het Bimhuis (nid 75459) mét
gevulde agenda.
🔴 Nog één keer over: tel-04-horeca.png toont nog de AdMob-testadvertentie
(showsTestAd: true in de export). Zodra Show Test Ads uit staat schiet
Claude die ene opnieuw — dat is 2 minuten werk, geen nieuwe build nodig
(deep link horecagelegenhedenOverzichtCurrent?plaats=28695).
🔸 Opgevallen, geen blokker: op de detailpagina's (tel-02-event,
tel-05/06-zaak) toont de header Amsterdam (de default
gemeenteSelectId), terwijl Home Heel Nederland zegt. Voor een event in
Steenwijk oogt dat wat vreemd; als je dat consequent wilt, is dat een aparte
taak op de header-keten.~/uk-play-assets/feature-graphic/DEFINITIEF-feature-graphic-1024x500.png
— 1024×500, RGB zonder alpha, 88 kB; voldoet aan de Play-eisen.
B = het complete app-icoon links als afgeronde tegel op leisteen
(#3E454C), rechts de naam + tagline + oranje accentstreep. Rood op
leisteen geeft het icoon contrast; de twee andere merkkleuren doen de rest.
Variant A (merkelement groot op merkrood) staat ernaast, niet gekozen.
feature.py staat erbij: tekst of kleur wijzigen is één regel + opnieuw
draaien. Rest nog: uploaden bij G3.2.#
com.uitgaanskrant.app — onveranderlijk na de eerste
upload, dus controleer 'm.#
https://uitgaanskrant.com/nl/support/privacybeleid
(anoniem 200, gemeten 2026-09-16).https://uitgaanskrant.com/nl/support/mobieleapp
⚠️ NIET /nl/account-verwijderen — die geeft anoniem 403 en reviewers
openen 'm uitgelogd. Taak 85 heeft de in-app-knop al naar dezelfde URL gezet.INTERNET, CAMERA, READ_EXTERNAL_STORAGE,
WRITE_EXTERNAL_STORAGE, plus via AdMob/Firebase ACCESS_NETWORK_STATE,
WAKE_LOCK, FOREGROUND_SERVICE, AD_ID en drie ACCESS_ADSERVICES_*.
Declareren: Advertising ID → JA (verplicht, AD_ID staat erin),
crashlogs/diagnostiek (Crashlytics), gebruikersnaam/e-mail (login), en
foto's (camera + galerij, voor de aanmaakpagina's). Transport is
versleuteld en verwijdering is mogelijk → beide aanvinken.#
assets/images/app_launcher_icon.png kun je
NIET rechtstreeks uploaden: die is 1900×1900 en 1,1 MB, terwijl Play
exact 512×512 en max 1 MB eist (gemeten 2026-09-20).
✅ Klaargezet: ~/uk-play-assets/play-icon-512.png — 512×512, RGB,
110 kB, LANCZOS-verkleind uit datzelfde bronbestand (dat volledig
ondoorzichtig is, dus er is niets platgeslagen). Gebruik die, en niet
de adaptive foreground.
🔸 Opgevallen, jouw keuze: het woordmerk beslaat maar een smalle strook
van het rode vlak. Op 512 is het scherp, maar in de Play-lijst wordt het
icoon rond de 48 px getoond en dan is "Uitgaanskrant.com" niet meer te
lezen — je ziet dan vooral een rood vlak. Een variant met alleen het
kalendertje groot (of de tekst fors groter) leest daar beter; dat is wel
een ontwerpbesluit, geen fout.#
1.0.0+1
(pubspec.yaml:18). Bij de FlutterFlow-route: App Settings → App Details.flutter_launcher_icons wél? Lokaal
gebouwd draagt het bestand anders het Flutter-vogeltje. Niet aannemen.#
Ja, betaald — en anders dan Google een ABONNEMENT. Google Play is eenmalig $25; het Apple Developer Program kost ~$99 per jaar en moet elk jaar verlengd worden, anders verdwijnt de app uit de App Store. Verifieer het actuele bedrag bij inschrijving.
Wat je al hebt en meeneemt:
Info.plist staan al goed (Camera, PhotoLibrary,
Calendar) — geverifieerd 2026-09-19, precies 3 keys.Wat er nieuw bij komt:
ios/Runner.xcodeproj/project.pbxproj
beweegt mee in elke export.Route wanneer het zover is: FlutterFlow kan ook voor iOS bouwen, dus een Mac is niet strikt nodig. Punt 5 en 6 zijn het echte werk; de rest is administratie.
Verse export bevestigt de body-Column van PUitgaanPage:
[slider (if !zoekOpen), Container > AdBanner, Expanded > FilterBalkComponent,
Expanded > lijst]. Precies de gevraagde volgorde.
Eindstand in de export: Container(height: 65/65/105/105) > Padding(top 15) >
FlutterFlowAdBanner zonder eigen width/height (dus adaptive). De banner houdt
daarmee zijn volle 50 dp op telefoon en 90 dp op tablet over, en de 15 dp lucht
tot de slider — die Bob er bewust in had gezet — blijft staan.
🔸 De zwarte laadtekst van FlutterFlowAdBanner blijft hoger dan de Container en
tekent dus even buiten zijn kader. Inherent aan de gegenereerde widget, niet vanuit
de builder te clippen. De layout beweegt niet meer, en dat was het doel.
Afgerond 2026-09-20 (Claude, in de builder, 19/19 geverifieerd met een verse export). Punt A (gewoon fout), punt B (technische veldnamen) en punt C (het sterretje bij de categoriekeuze) zijn gedaan op béide pagina's:
| was | is nu |
|---|---|
ACT kop "Categorie evenement" |
"Categorie activiteit *" / Category * |
EVT kop "Categorie evenement" |
"Categorie evenement *" / Category * |
| "Selectee..." | "Selecteer..." |
| "Datum eind" (4×) | "Einddatum" / End date |
| "WebsiteURL" (2×) | "Website" |
| "TicketsURL" | "Ticketlink" / Ticket link |
| "Toelichting Entree" (2×) | "Toelichting entree" |
| "Uw Horecagelegenheid" | "Je horecagelegenheid" / Your venue |
EN Zipcde · Organisatsion · Feeprice (2×) |
Zipcode · Organisation · Admission price |
EN Title cityactivity · Title cityactvity |
Activity title · Event title |
Nog open — schrijfkeuzes, bewust bij Bob gelaten:
Text-label
bóven het invoerveld én een labelText én een hintText; staan die alle drie
op hetzelfde woord, dan leest de gebruiker het drie keer. Bob heeft de losse
Text-labels van "Toelichting Entree" en "Entreeprijs" al weggehaald; over
zijn nog de label/hint-paren: ACT Einddatum · Adres · Postcode ·
Entreeprijs, EVT Einddatum · Toelichting entree · Entreeprijs. Een voorbeeld
in de hint doet meer werk dan een herhaling — "bijv. 7,50", "1012 AB".ACT EN Description activity tegenover EVT EN Event description.Select... staat er
nog 4× (ACT xiba4gka, l9f37oxb, ajotnz1f; EVT 2hitlrxa, ht4toe4o) en
Search... 1× (EVT qtnlg7nd), terwijl de categorie-dropdown op ACT wél
"Selecteer..." / "Zoek..." zegt. Mechanisch werk; Claude kan dit oppakken.EVT. De intro zegt "van je
onderneming", de kop en de dropdown zeggen "horecagelegenheid", de
hulpregel zegt "de zaak waar het evenement plaatsvindt". Eén woord kiezen
is een besluit voor Bob, geen opruimklus.stadsactiviteitAanmaken — Drupal-deel · Eigenaar: BobCode staat klaar en is getest (herschreven 2026-09-24, want de versie van
2026-09-20 was nergens bewaard): snippets/mijn-stadsrechten-keten.inc.txt in
dit repo, én al op de server in ~/tmp/ (o1). Daar staat ook
~/tmp/evenementen_aanmaken.NIEUW.inc: het complete bestand met de nieuwe
functie er al in (regels 316-373 vervangen), php -l schoon, 16 functies zoals
het origineel. Read-only doorgedraaid tegen bobcity (4× gemeente), bobhoreca
(provincie + gemeente) en Bee (0 rechten): klopt.
Deploy (Bob, één blok):
cd /data/disk/o1/static/uitgaanskrant/sites/all/modules/custom
cp custom.evenementen_aanmaken.inc ~/tmp/custom.evenementen_aanmaken.inc.bak-$(date +%F)
diff custom.evenementen_aanmaken.inc ~/tmp/evenementen_aanmaken.NIEUW.inc | grep "^[0-9]" # alleen 316-373 hoort te verschillen
cp ~/tmp/evenementen_aanmaken.NIEUW.inc custom.evenementen_aanmaken.inc
drush @uitgaanskrant.com cc all
Terugrollen: de .bak terugkopiëren + cc all.
Waarom: field_town_access mag een term op elk van de drie niveaus van
de town-vocabulary bevatten — plaats (tot 2026-08-27), gemeente (huidige
standaard) of provincie (de widget staat op Max depth 2 en laat dat toe; Bob
geeft ze bewust op provincieniveau, dat scheelt veel werk). De app stopte die tid
rechtstreeks in plaatsen_bij_gemeente, en dat geeft bij provincie én plaats een
lege lijst → zie taak 125.
De nieuwe versie geeft per recht de volledig uitgeklapte keten terug:
niveau · provincie_tid/_titel · gemeente_tid/_titel ·
plaats_tid/_titel · keten ("provincie|gemeente|plaats", ontbrekende delen
leeg) · label (bv. "Noord-Brabant (hele provincie)"). tid, titel,
parent_tid en parent_titel blijven ongewijzigd staan, dus bestaande
consumenten merken niets. Sortering is hiërarchisch i.p.v. alfabetisch op titel.
Getest op productie onder een testnaam, alle drie de niveaus:
| account | recht | keten |
|---|---|---|
| bobcity | 28694 Amsterdam (gemeente) | 28666\|28694\| |
| bobhoreca | 27409 Noord-Brabant (provincie) | 27409\|\| |
| Team Klein Frankrijk | 29912 Zuid-Holland (provincie) | 29912\|\| |
De eerste dropdown in het Waar-blok van stadsactiviteitAanmaken moet de drie
cascade-dropdowns voorvullen tot zover het recht reikt, in plaats van alleen
createGemeenteID te zetten:
| recht | voorvullen | gebruiker kiest nog |
|---|---|---|
| provincie | provincie | gemeente + plaats |
| gemeente | provincie + gemeente | plaats |
| plaats (oud) | alle drie | niets |
Stappen: dropdown-waarde omzetten van $[:].tid naar $[:].keten (label
op $[:].label), plus drie custom functions van één String-argument
(ketenProvincie / ketenGemeente / ketenPlaats → split('|')[n]). Die ene
parameter is bewust: een lookup op de hele respons vraagt een custom function met
meerdere argumenten, en die binding loopt in de Action Flow Editor structureel
vast (zie CLAUDE.md). On Selected → Update Page State voor
createProvincieID/createGemeenteID/createPlaatsID + Set Form Field op de
drie dropdowns.
Een lege Set Form Field-waarde wordt geweigerd ("Value cannot be empty"), dus
voor de lege delen van de keten is Reset Form Fields nodig — wat meteen de
tweede helft van taak 126 afhandelt.
Voorvullen, niet verbergen (besluit Bob 2026-09-20): met verbergen kun je achteraf niet meer wisselen zonder eerst de rechten-dropdown leeg te maken.
stadsactiviteit_aanmaken_widget.dart:1878 doet
(getJsonField(respons, r'$[:].plaatsid', true) as List?)!. getJsonField geeft
bij 0 matches null — ook met isForList: true, want de isEmpty-check
staat ervóór ([flutter_flow_util.dart:356]). Een lege respons [] geeft dus
Null check operator used on a null value; in een release-build een grijs blok
zonder melding.
Treedt nu op bij elke provincie- of plaats-tid in field_town_access, maar staat
los van taak 123/124: ook een gemeente zónder plaatsen in de taxonomie geeft
dezelfde crash. Zelfde patroon staat op de gemeente- en provincie-dropdown
van diezelfde pagina — alle drie langslopen.
Fix — exact recept (Claude 2026-09-24, na meting op de export). ⚠️ Een
Predefined Path is hier NIET genoeg: Define Options is een non-nullable
List<String>, dus FlutterFlow plakt er sowieso een ! achter (zie de bestaande
functions.categorieSubTids(...)! op dezelfde pagina's — die crasht bij een lege
respons net zo hard). Wat wél werkt is een custom function met een non-nullable
returntype: dan komt er een kale functions.x(...) zonder !, en de functie geeft
zelf [] terug bij null/leeg.
Custom Code → + → Function lijstVeld. Argumenten: respons (type JSON,
Nullable aan), veld (String, Nullable uit). Return Value: type String,
Is List aan, Nullable UIT (anders komt de ! terug). Body (plat, geen
closures — FlutterFlow's validator struikelt daarover):
final List<String> uit = [];
if (respons is! List) {
return uit;
}
for (final item in respons) {
if (item is Map && item[veld] != null) {
uit.add(item[veld].toString());
}
}
return uit;
Ctrl+S, dan in het Issues-paneel de regel "Custom functions need to be checked" aanklikken.
lijstVeld → argument respons = de Backend-Query-respons van die dropdown
(API Response Options JSON Body, Available Options No Further Changes),
argument veld = letterlijke tekst uit de tabel. Confirm.| pagina | DropDown | Values veld |
Labels veld |
|---|---|---|---|
uitgaansevenementAanmaken |
Horecagelegenheid | nid |
titel |
uitgaansevenementAanmaken |
Entree | tid |
naam |
stadsactiviteitAanmaken |
Stadsrechten (in Column…MijnStadsrechten) |
tid |
titel |
stadsactiviteitAanmaken |
Provincie | provincieid |
provinciename |
stadsactiviteitAanmaken |
Gemeente | gemeenteid |
gemeentename |
stadsactiviteitAanmaken |
Plaats | plaatsid |
plaatsname |
stadsactiviteitAanmaken |
Entree | tid |
naam |
grep -c "as List?)!" <pagina>_widget.dart
hoort 0 te geven (nu 4 resp. 10), en grep -c "functions.lijstVeld(" 4 resp.Initial Option Value) blijft ongemoeid; alleen de
optie-lijsten wisselen van bron.createPlaatsID bij het wisselen van gemeente · Eigenaar: nader te bepalenGeen van de vier dropdowns in het Waar-blok reset de onderliggende. Scenario:
kies gemeente A → plaats A₁ (createPlaatsID gezet) → wissel naar gemeente B.
De Plaats-dropdown toont dan weer zijn hint (FlutterFlowDropDown filtert een
waarde die niet in de opties zit weg, [flutter_flow_drop_down.dart:101]), maar
createPlaatsID houdt A₁ vast. De verzendknop blijft dus zichtbaar en je
dient in voor de vorige plaats. Stil, geen foutmelding.
Fix: Reset Form Fields + Update Page State (Reset Value) op de onderliggende
velden in de On-Selected van provincie, gemeente én de rechten-dropdown. Overlapt
met taak 124 — samen oppakken.
Gemeten op de verse export van 2026-09-20 (dode kopieën uitgefilterd). Dit is de staart van P1-33: daar is destijds het rood opgeruimd, grijs en groen bleven staan.
| kleur | = token | aantal | waar |
|---|---|---|---|
Color(0xFFEEEEEE) |
Alternate | 9 | kaartranden op uitgaansevenementAanmaken (4) en stadsactiviteitAanmaken (5) |
Color(0xFF09B34A) |
Tertiary | 2 | HorecagelegenheidoverzichtKaart + tagCategorieComponent (het groene categorielabel) |
Nu onzichtbaar — de hexwaarden zijn exact gelijk aan de tokens, dus er verandert
niets aan het beeld. Het punt is onderhoud: wijzig je ooit alternate of
tertiary in Theme Settings, dan lopen deze elf plekken niet mee en krijg je twee
tinten door elkaar.
Doen: per widget het kleine kleurblokje vóór de kleurnaam aanklikken (niet de tekst — dan maak je er weer een hex van) → in "Choose a Color" onderaan de lijst Theme Colors → Alternate resp. Tertiary. Sluit met de X, niet met Escape (Escape laat de tree-selectie terugspringen naar de pagina-root).
⚠️ Controle 2026-09-24 avond, twee verse exports: Bob's ronde is niet doorgekomen —
zelfde 8 hexen als ervoor. Herkenning in de builder: na een geslaagde wissel toont het
Border Color-veld het woord Alternate (met een blokje ervoor), niet #eeeeee.
Staat er nog een hex, dan is de tekst bewerkt i.p.v. het blokje. Verifieer per kaart
in het paneel vóór je verdergaat; Claude checkt daarna met
grep -c "0xFFEEEEEE" <widget>.dart (hoort 0) in een verse export.
Stand 2026-09-24 middag: uitgaansevenementAanmaken ContainerWat, ContainerOrganisatie
en Container (Entree) staan op Alternate (export: 1× 0xFFEEEEEE over, dat is
ContainerWanneer — daar landde de klik op de pagina-root). ContainerMedia stond al
goed. Nog te doen: ContainerWanneer + de 5 kaarten op stadsactiviteitAanmaken + de
2 groene.
Vindplaatsen, gemeten op de export van 2026-09-20 — het gaat steeds om de Border color van de kaart-Container om een formuliersectie, niet om de Fill:
| pagina | sectie waar de kaart bij hoort |
|---|---|
uitgaansevenementAanmaken |
intro ("Meld hier een evenement aan…") · zoekveld · "Datum eind" · "Link om kaarten te kopen" |
stadsactiviteitAanmaken |
intro ("Meld hier een activiteit aan…") · zoekveld · "Datum eind" · "Plaats activiteit" · "Website van de activiteit" |
De twee groene (09B34A → Tertiary) zijn wél Fill Colors:
HorecagelegenheidoverzichtKaart en tagCategorieComponent, allebei het
categorielabel.
De 12e kleur slaan we over: Color(0xFFFACFC6) in een gradient op
SliderUitgaanComponentSmallCurrent. Dat is roze, hoort bij géén token, en dat
component heeft nul gebruikers — het staat al op de opruimlijst.
⚠️ Claude kwam hier 2026-09-20 niet doorheen. Navigeren, panelen wisselen en de linkerrail werkten normaal, maar geen enkele klik op een widget-tree-rij selecteerde iets — ook de root niet, in ~10 pogingen op wisselende x/y. Een klik op de chevron ernaast deed wél iets (deselecteerde de root), dus de events komen aan; alleen de rij-selectie niet. Zonder selectie verschijnt het eigenschappen-paneel niet en is er geen kleur te zetten. Bob's venster was toen 1266x952; een breder venster is het eerste om te proberen.
Geen haast, geen blokker — prima klusje voor tussendoor.
Bob heeft Calendar uitgezet in App Settings → Permissions. Verse export bevestigt:
android/app/src/main/AndroidManifest.xml heeft nog vier permissies
(INTERNET, CAMERA, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE) en
ios/Runner/Info.plist nog twee usage descriptions (Camera, PhotoLibrary).
READ_CALENDAR/WRITE_CALENDAR en NSCalendarsUsageDescription zijn weg.
Rest: de agendaknop op EventCurrent één keer testen op een echt toestel
(de AVD's kunnen dit niet). En bij een latere iOS-ronde de permissie terugzetten —
daar gebruikt add_2_calendar wél EventKit.
De uitgelogde doorloop is af (2026-09-19, --release, flags=0x0): 0 FATAL,
0 E/flutter, alle iconen renderen, vliegtuigmodus geeft netjes de
VerbindingsBanner, Engels vertaalt. 65a, 65b en 65d zijn opgelost en
export-geverifieerd.
Wat overblijft — en dat is per definitie Bob's stuk, want Claude voert geen wachtwoorden in:
Meenemen in die doorloop (nieuw, 2026-09-20): de gebruikersnaam wordt nu
onthouden na uitloggen. Verwacht gedrag: inloggen → uitloggen → het bovenste
veld op Login staat al ingevuld met wat je de vorige keer typte, het
wachtwoordveld is leeg. Overleeft ook het volledig afsluiten van de app
(persisted). Het kruisje in het veld wist het voor een andere gebruiker.
🔴 De ingelogde doorloop. Nooit op een release-build getest, en het raakt de kernfuncties: inloggen · hartje op een zaakpagina (aan én uit) · Favorieten met inhoud, alle vier de tabs · gemeente volgen · evenement aanmaken tot en met indienen · Mijn profiel inclusief uitloggen. Een Play-reviewer logt straks wél in.
65c · ✅ af (nagemeten 2026-09-19 avond in een verse export): Favorieten toont
uitgelogd nu een uitlegtekst met inlogknop, achter een guard op userSessionid
(sleutel zxbjvxzq). Gebouwd in een andere sessie als taak 107.
65e · "Datum" blijft Nederlands in de Engelse modus — het label komt uit de
valueOrDefault-default 'Datum' en is dus geen vertaalsleutel. Hoort bij P1-17.
Cosmetisch, al bekend: het logo in de drawer staat deels onder de statusbalk op de release-build (op de profile-build van 2026-09-19 is dat niet meer zo — taak 100 zette de padding op 40; opnieuw bekijken bij de volgende release-build).
https://uitgk.com/<nid> (deelknop op EventCurrent en
de tekst in zetInAgenda) redirect naar de juiste pagina op uitgaanskrant.com.
Getest op drie nids, inclusief een horecagelegenheid: 222338 -> "Scott Hamilton
& Rein de Graaff Trio…", 75459 -> "Bimhuis Amsterdam". Eind-URL is een nette
/nl/Noord-Holland/Amsterdam/...-alias./nl/support/mobieleapp (de Support-link in de
drawer én de data-deletion-URL voor Play) en /nl/support/privacybeleid.devbob staat niet meer in de levende app-code (alleen in dode kopieën).Fase 1 én 2 staan op productie en zijn nagemeten. Van 42 naar 22
methodes op endpoint flutterdrup. Het bewezen lek is weg: node/<nid>.json
gaf anoniem 200 met 46 velden inclusief name (de inlognaam van de auteur,
dus user enumeration) en geeft nu 404.
Dicht (404): node/retrieve + relationships/files · file/create +
file/retrieve · taxonomy_term · taxonomy_vocabulary · user/retrieve|create|
delete|logout|token|user_pass_reset|cancel|password_reset|resend_welcome_email ·
system/connect · geocoder · userestablishments.
Blijft aan (22): de app-resources (plaatsen, plaatsen_bij_gemeente,
horecacategorieen, views, de favorieten-, aanmaak- en upload-acties,
mijn_*, categorieen, entreeopties, accountdelete) plus user/login en
user/request_new_password.
Regressiecheck na afloop, devbob én productie: alle vijf views-endpoints 200 ·
plaatsen/plaatsen_bij_gemeente/horecacategorieen 200 · POST user/login.json
401 op onzin · een afbeelding uit een view 200 image/jpeg.
📁 Script: /home/bob/Projects/services-dicht-fase2.php (droogdraai is de default,
doen als argument voert uit). Bewaren voor als er ooit een resource bij komt.
Alleen de vijf app-views geven 200; user_contents, places, _town_tree,
thuis_bezorgen, front_block_go_ca, _for_search_page, establishment_calendar,
flutterfavorietenagenda, _block_go_ca_for_town, block_more_go_out,
latest_photobook en _search_ca_page geven 404.
⚠️ De foutmelding luidt ["Display block_1 on view user_contents could not be
found"] terwijl die view wél een block_1 heeft — dat betekent hier "view staat
niet op de toegestane lijst", niet "display bestaat niet".
Alle vijf nagemeten op de verse export en op productie; niets te doen, hier genoteerd zodat niemand het opnieuw uitzoekt:
~/uitgaanskrant-test.apk met readelf -lW: libapp.so en
libflutter.so zijn 0x10000 (64 KB) uitgelijnd, libdatastore_shared_counter.so
0x4000 (16 KB) — allemaal ≥ 16 KB. ndkVersion 28.2.13676358 dekt dit.36 (android/app/build.gradle:62), ruim boven de Play-eis van 35./nl/support/privacybeleid
(200) noemt AdMob (2x), Crashlytics (3x), Firebase (5x), advertentie (6x) en
verwijderen (8x). Dat is precies wat Play's Data-safety-controle naast je
declaratie legt./nl/user/register
(de knop Account aanmaken op Favorieten) en /nl/user/password. Zelfde val als
bij G2.2, maar hier dus goed.main.dart:32) — of het bericht ook echt
verschijnt hangt af van de AdMob-console, zie taak 118.AdMob: Show Test Ads uitzetten. App Settings → AdMob (niet een vinkje
op het AdBanner-widget — dat paneel heeft alleen Ad Properties en Ad Banner
Dimensions; de export schrijft de projectinstelling per widget uit).
Twee dingen bij het omzetten: (1) reken op lege ruimte i.p.v. advertenties
zolang de app niet gepubliceerd is — AdMob levert dan nauwelijks fills, dat is
normaal; (2) klik daarna niet zelf op je eigen advertenties, dat telt als
ongeldig verkeer. Zodra hij uit staat schiet Claude tel-04-horeca.png opnieuw.
Builder-opruimronde, laatste restanten. Nog in de export: componenten
SliderUitgaanComponentSmallCurrent,
PUitgaantabelKaartComponentOrgineelMetKaartjeerin en
SelectStateDropDownComponentCopy2, de pagina mijnProfielCopy3 (staat alleen
als route in nav.dart, niets navigeert ernaartoe) en de custom function
dISABLEdatumVoorApi. (De laatste twee kopieën ontstonden 2026-09-20 bij het
oefenen op taak 2-4.)
⚠️ Pagina Event: laten staan, dit is een doodlopend spoor. Bob bevestigde
2026-09-20 dat óók hernoemen afketst op "Invalid Action" — verwijderen,
verplaatsen en hernoemen falen dus alle drie, ook in zijn eigen browser.
Nagemeten op de export (2026-09-20): de pagina is onbereikbaar. De enige
verwijzingen zijn de twee die FlutterFlow automatisch voor élke pagina
genereert (index.dart en de route-registratie in nav.dart); er is nergens
een goNamed/pushNamed naartoe. Geen enkele gebruiker komt er dus, behalve
via een handmatige deep link. Kosten van laten staan: één dode route in
nav.dart en ~30 kB in de app. Niet opnieuw agenderen. Moet in de builder, niet met
git rm — anders staat alles na de volgende export terug.
8mxemaqc en q8einmt3 (nl Kopieer naar een nieuw evenement, en staat op
Copy to a new activity, moet Copy to a new event; adjt2leo is goed) · de
vier lege-lijst-teksten op mijnProfiel (parameterwaarden, geen globe) · de
Select.../Search...-placeholders uit 122-F · "Datum" uit 65e.
⚠️ App Settings → Languages werkt ook in Bob's browser niet (2026-09-24):
ga per pagina/component via het globe-icoontje op het widget. Controleer de
ronde met een diff van internationalization.dart tussen twee exports.P1-17 · vertalingen: ✅ rond voor de app (2026-09-20). Nagemeten over alle
218 sleutels die in bereikbare widgets gerenderd worden: 0 met gevulde NL en
lege EN. De laatste zes zaten in FilterBalkComponent en zijn ingevuld.
tcyc3idz bestaat niet meer. Blijft over: de vier lege-lijst-teksten op
mijnProfiel, die zijn letterlijke parameterwaarden zonder globe.
⚠️ Tel geen sleutels mee waar EN == NL terwijl dat correct is (Select...,
Website, Facebook, Media, ", " — identiek in beide talen).
Titels met spaties vooraan/achteraan (was P1-47). Bob 2026-09-11: de
impact is te groot, we lopen de namen door bij de livegang. MySQL sorteert op
de rauwe node.title, dus zo'n node valt buiten de alfabetische volgorde
terwijl de JSON schoon oogt (_custom_clean_html poetst ná de query).
Telquery:
```
drush @prod sqlq "SELECT type, COUNT(*) FROM node WHERE title <> TRIM(BOTH ' ' FROM TRIM(BOTH CHAR(9) FROM TRIM(BOTH CHAR(10) FROM TRIM(BOTH CHAR(13) FROM TRIM(BOTH ' ' FROM title))))) GROUP BY type;"
De fix is een `UPDATE` met diezelfde geneste `TRIM` op **`node` én
`node_revision`**, idempotent, met vooraf een `sql-dump` van die twee tabellen.
URL-aliassen blijven ongemoeid.
- **Max items + tab 6** (P2-24 A en C) — ✅ **opgelost**, alle zes de tabs draaien
op `filterHorecagelegenheden` met `.take(1000)`. Niets meer te doen.
*(Afgerond en daarom verwijderd uit dit blok: taak 56 opruim-cron · P1-30
Cloudflare rate limiting · P2-25 debug-cleanup `custom.module` · de twee dode
KANWEG-API-calls · de opruimronde van 7 dode kopieën.)*
### P2-46 · EventCurrent: restpunten na de ombouw van 2026-09-19 · Eigenaar: Claude
**Gedaan (export- en toestel-geverifieerd, profile-build op emulator-5554):** backups `EventCurrentCopy`, `EvenementHorecagelegenheidCopy`, `EvenementInfoCopy`; event-kaart met logo 96×96 links en `EvenementInfo` (Expanded) + Maps-knop rechts; de 500 px-Container weg; `EvenementHorecagelegenheid` zonder tabs: `RowLogo` (logo 96×96 + `ColumnRechts`: titel, labels, adres/plaats, telefoon, e-mail), links in een `Wrap` (Menukaart/Website/Facebook/Instagram/X/"Op Uitgaanskrant.com" met host ervoor), `TextInhoud` (zaakbeschrijving), foto's, `ColumnOpening` en `ColumnBezorgen` met Visibility op `openingstijden`/`bezorgtijden`; e-mail leest nu `$[:]['e-mail']`; drie `phone: false`-vlaggen weg; telefoon-guard zonder `valueOrDefault`; Entree-blok alleen bij `entreeprijs`; roze/rode vulkleuren van Entree/Contact weg; EN-vertalingen "Opening hours"/"On Uitgaanskrant.com".
**Nog te doen:**
- ✅ **Laatste toestelronde gedaan (2026-09-19, profile op emulator-5554, `horecaid=157070`).**
`RowLogo` (logo 96×96 + titel + categorielabel), de adres-Row `[Icon, TextAdres, TextPlaats]`
met spacing 6 ("Westerstraat 95 Enkhuizen"), telefoon, e-mail, de links-`Wrap`
(Menukaart/Website/Facebook/Instagram/X/Twitter/Op Uitgaanskrant.com) en `TextInhoud`
renderen allemaal goed.
✅ **"Contact"-label gefixt (2026-09-19 avond):** de Row staat nu op Cross Axis
Alignment *start* met Items Spacing 8, dus het label lijnt uit met de eerste regel
links i.p.v. tussen de regels te hangen. Export: `crossAxisAlignment: start` +
`.divide(SizedBox(width: 8.0))`.
- ✅ **`ColumnBezorgen` mét echte data bekeken en grotendeels opgeknapt (2026-09-19).**
**Testzaak: nid `157070`** (Chinees-Indische restaurant Lotus, Enkhuizen) — die heeft
álle bezorgvelden gevuld (`bezorgtijden`, `bestellink`, `bezorgkosten`, `minimaleorder`,
`bezorgtin`, `thuisbezorgtbetaalopties`, `cryptocoins`, `afhaalopties`) én `menukaart`,
`openingstijden`, `openingstijdenuitzondering` en `kvk`. Route:
`uitgaanskrant://uitgaanskrant.com/eventCurrent?nid=214689&horecaid=157070`.
**Wat er mis was:** de sectie toonde elf kale regels zónder één label — "vanaf 17 uur",
"2 euro", "20 euro", "Contant" zonder context, de bestellink als kale URL, geen kop, en
lijstwaarden die aan elkaar plakten ("AfhaalBezorgen", "EnkhuizenBovenkarspel").
**Gefixt en op emulator-5554 (profile) geverifieerd:** kop **"Bezorgen"** (i18n `jxlwje5u`,
dezelfde stijl als "Openingstijden"); labels via Combine Text op *Bezorgtijden:*,
*Bezorgkosten:* en *Minimale bestelling:*; **een Visibility-guard op
`Textbezorg-minorder`** (`establishmentMinimaleorder is set/non-empty`) — die had er géén,
dus met label erbij zou hij anders "Minimale bestelling: null" tonen; en **Wrap-spacing 8**
op de drie lijsten, zodat de waarden niet meer aan elkaar plakken.
- ✅ `TextInhoud` heeft nu een kop **"Over deze zaak"** (EN *About this venue*, sleutel `rko8u7a5`, Body Medium 600) in een nieuwe `ColumnInhoud` met padding-top 8 (2026-09-19 avond, export-geverifieerd).
- ✅ EN-vertaling van "Menukaart" (`22xoyrqe`) staat nu op **`Menu`** (2026-09-19 avond).
- Niet getoond: `kvk` (zaak). Bewust overgeslagen.
- ✅ **`organisator` wordt nu wél getoond (2026-09-19, Bob's besluit, af en op toestel geverifieerd).**
Zie het eigen blok hieronder; taak 99 kan hiermee dicht.
- Bob: de drie backup-kopieën (`EventCurrentCopy`, `EvenementHorecagelegenheidCopy`, `EvenementInfoCopy`) op de P2-7-opruimlijst zetten zodra de nieuwe pagina goedgekeurd is. Claude verwijdert niets.
## 🗄 Drupal / views — open
#
### 📌 Het horeca-overzicht laadt AL lazy — één fetch bij openen, niet zes
**Gemeten 2026-09-19 op de export; dit corrigeert een eerdere bewering van Claude
in deze sessie ("tot 60 requests per schermopening").** Die telling was fout: er
staan zes `fetchAlleHorecagelegenheden`-aanroepen in
`horecagelegenheden_overzicht_current_widget.dart`, maar ze worden niet samen
uitgevoerd.
- **On Page Load** (regel 55, `addPostFrameCallback`) doet er **één** — tab 0
(`horcat 17969`, Activiteiten, `services_1`).
- De **TabBar** heeft `onTap: (i) async { [ () async {}, …vijf closures… ][i]() }`
— dus per tab-tik precies één fetch, en tab 0 doet niets omdat die al geladen is.
**Een schermopening kost dus één `fetchAlleHorecagelegenheden`**, en die loopt
door API-pagina's van 100 tot `maxPaginas = 10`. In Amsterdam heeft tab 0
(Activiteiten) 42 items → **één HTTP-request**. De 60 uit die eerdere schatting
komt in de praktijk nooit voor.
**Gevolg voor de rate limit:** 100 requests per 10 s is ruim voldoende; de
verhoging naar 200 was niet nodig (schaadt ook niet).
**Wat er wél nog te winnen valt, klein:** elke tab-tik doet een **verse** fetch,
ook als je terugkeert naar een tab die je al bezocht hebt. Heen-en-weer klikken
tussen zes tabs haalt dus zes keer opnieuw op. Een guard ("sla de fetch over als
`alleXxx` al gevuld is") lost dat op, maar kost een conditie in vijf
actieketens — en juist die dialoog is op deze pagina fragiel. **Niet gebouwd;
alleen doen als het in de praktijk hindert.**
### P2-24 · Restpunten horeca-zoekfilter · Eigenaar: zie per punt
**A, C, D en E zijn af** (2026-09-19, export-geverifieerd): alle zes de
`StaggeredView`s draaien op `filterHorecagelegenheden(...)`, `.take(25)` is weg
(6x `.take(1000)`), de routes wijzen naar `...Current`, en de twee lege
`TextField`-placeholders zijn opgeruimd. **F is geschrapt** op Bob's verzoek.
**B — categoriedropdown per tab: ✅ af** (nagemeten 2026-09-19 avond): de
`FlutterFlowDropDown` op `horecagelegenhedenOverzichtCurrent` hangt aan
`horcatOptiesVoorTab` / `horcatLabelsVoorTab` met de TabBar-index. Daarmee is P2-24
helemaal rond; dit blok kan weg zodra Bob 'm op het toestel goedkeurt.
**Niet oplosbaar, geaccepteerd:** FlutterFlow zet op de `On Change`-trigger van
het zoekveld een **`EasyDebounce` van 2000 ms** die nergens instelbaar is. De
lijst ververst dus ~2 s nadat je stopt met typen.
### 🔴 Taak 28 / 109 · 618 toekomstige events zonder categorie komen NERGENS in de app · Eigenaar: Bob
**Nagemeten door Claude op productie, 2026-09-19 17:44** (alle zeven displays
volledig gepagineerd, nids naast `flutterflow_events` gelegd, geclassificeerd op
**datum + tijd** tegen de `>= -2 uur`-grens van de master):
| | |
|---|---|
| echte toekomstige events | **6.684** |
| bereiken geen enkele tab | **618 (9 %)** |
| ... daarvan zonder categorie | **618 — dus 100 %** |
| ... daarvan mét categorie | **0** |
**Twee dingen die dit uitwijst.** (1) De zeven displays dekken **alle**
categorieën correct af — er is geen categorie meer die nergens landt, dus dát
deel van taak 28 is klaar. (2) Het hele resterende lek is één oorzaak: een event
**zonder** categorie valt door elk display-filter heen en is in de app dus
onvindbaar. Stand 12 sep: 597. Nu: 618. Het loopt dus licht op — de fix van
19 sep werkt kennelijk niet (of alleen voorwaarts, terwijl de bestaande nodes
blijven staan).
Voorbeelden (allemaal ruim in de toekomst, dus geen datumkwestie):
`222806` Steve 'n' Seagulls (20 dec, Weert) · `222737` Altruism Label Night
(21 nov, Maastricht) · `222703` BRECHTJE Maresa (1 okt, Groningen).
**✅ Besluit Bob 2026-09-19: dit is een IMPORT-probleem en wordt bij de importer
afgevangen.** Geen Drupal-hook of extra display nodig; niet opnieuw agenderen.
Wel bij een volgende contentmeting nakijken of het getal daalt (nu 618).
<details><summary>afgewezen alternatief</summary>
**Voorstel — een default in `custom_node_presave()`.** Die hook draait er al
voor `go_out_event` (adres/geo/plaats overnemen van de horecagelegenheid); één
blok erbij dat `field_category2` vult met een vaste fallback-tid zodra het leeg
is, dekt alle nieuwe imports. De 618 bestaande haal je in één keer mee met een
`drush php-eval` die diezelfde tid zet en `node_save()` aanroept.
Wat Claude van Bob nodig heeft: **welke tid de fallback moet zijn** (een
bestaande categorie, of een nieuwe term "Overig"). De alternatieven — een achtste
display zonder categoriefilter, of de importer aanpassen — kosten meer en geven
een extra tab in de app.
</details>
⚠️ **Meetles, kostte deze sessie een valse ronde:** classificeer op **datetime**,
niet op datum. Op datum alleen leverde dezelfde meting 30 "bugs" op die in
werkelijkheid gewoon events van vandaag waren die al begonnen waren — precies wat
het `>= -2 uur`-filter hoort weg te laten. Staat ook al in `CLAUDE.md`; ik trapte
er alsnog in.
### Taak 116 · Sessie-verlopen-melding één keer visueel bevestigen · Eigenaar: Bob
De 403-detectie (2026-09-20) is volledig geverifieerd op code- en serverniveau:
export bevat alle onderdelen, `dart analyze` 0 errors, en productie antwoordt bij
een ongeldige cookie met `403 ["Toegang geweigerd voor gebruiker anonymous"]` —
precies wat `drupalRequest` detecteert. **Alleen nog niet met eigen ogen op een
toestel gezien.** Geen aparte build voor starten; meenemen bij de eerstvolgende
keer dat er toch een build draait.
Test in drie stappen (~1 min):
1. Log in de app in en open Favorieten (moet normaal laden).
2. Wis de sessie server-side — vervang `<uid>` door je eigen uid:
ssh aegir-o1 "drush @uitgaanskrant.com php-eval \"db_delete('sessions')->condition('uid', )->execute();\""
3. Ververs Favorieten in de app. Verwacht: rode balk *"Je sessie is verlopen. Log
opnieuw in."* met knop **Inloggen**, en de pagina klapt naar zijn uitgelogde
weergave.
Werkt het niet, dan zit het in laag 2 (`VerbindingsBanner`), niet in de detectie —
die is hierboven al bewezen. Controleer dan `adb logcat | grep "SESSIE VERLOPEN"`:
staat die regel er wél, dan komt de melding niet door en is het de SnackBar-logica.
### Taak 115 · Autofill: wachtwoordmanager op de loginpagina · Eigenaar: Claude (Bob plant een losse sessie)
**Doel:** de gebruiker zijn inloggegevens door Android/iOS laten opslaan en
invullen, zodat opnieuw inloggen twee tikken kost. De app slaat bewust géén
wachtwoord op — dat blijft bij het besturingssysteem.
**Context:** sessies zijn sinds 2026-09-20 een jaar geldig (zie `CLAUDE.md`),
dus dit is comfort, geen noodzaak. Los oppakken, niet meeliften op ander werk.
**Stap 1 — invullen (FlutterFlow-property, laag risico).**
Pagina `login`, beide `TextFormField`s:
| veld | herkenbaar aan | Auto Fill Hint |
|---|---|---|
| gebruikersnaam | `obscureText: false`, regel ~227 in de export | `username` |
| wachtwoord | `obscureText: !_model.passwordFieldVisibility`, regel ~402 | `password` |
Zit in **Properties Panel → Additional Properties → Auto Fill Hint** (toggle
aan, dan de optie kiezen). Kies `username` en **niet** `email`: Drupal 7's
`user/login` accepteert standaard alleen de gebruikersnaam.
**Stap 2 — opslaan aanbieden (custom action, hier zit het risico).**
Alleen hints is niet genoeg: het OS vraagt pas "wachtwoord opslaan?" na
`TextInput.finishAutofillContext()`. FlutterFlow genereert die aanroep niet en
documenteert hem niet. Nieuwe custom action `autofillAfronden`, zonder
argumenten, aan te hangen **ná** een geslaagde `drupalLogin`:
```dart
import 'package:flutter/services.dart';
Future autofillAfronden() async {
TextInput.finishAutofillContext();
}
Open vraag: werkt dit zonder een AutofillGroup om beide velden heen?
FlutterFlow genereert die niet zichtbaar, en Android is daar in Flutter
historisch kieskeurig in. Lukt het niet, dan is de enige route een custom widget
dat het hele loginformulier vervangt — dat is een flinke ingreep en waarschijnlijk
niet de moeite waard voor puur comfort.
⚠️ Testen kan NIET op de emulators. De tablet-AVD heet
Medium_Tablet_no-google: geen Play Services, dus geen Google Password Manager.
Dit moet op Bob's fysieke Xiaomi, met een profile-build.
Terugdraaien (per stap, beide onafhankelijk):
autofillHints: uit lib/login/login/login_widget.dart —
controleer met grep -c autofillHints lib/login/login/login_widget.dart,
hoort 0 te zijn.grep -rc finishAutofillContext lib/ hoort 0 te geven.Geen van beide stappen raakt de sessie-afhandeling, drupalLogin of App State,
dus terugdraaien kan niet stukmaken wat nu werkt.
#
launchURL-aanroepen in
levende code hangen achter een guard op de rauwe waarde, niet op een
valueOrDefault.EstablishmentInfo geeft precies één rij per zaak (60 Amsterdamse zaken
getest). De zes $[:].veld-knoppen op de detailpagina leunen daarop; let op
zodra er een multi-value veld aan die view wordt toegevoegd.categorie is op alle zeven flutterflowmobiel1-
displays altijd een lijst (65 records gemeten).SliderUitgaanComponentSmallCurrent negeert zijn eigen
townid/displayid (P1-44-patroon), maar heeft nul gebruikers — geen
impact, alleen niet inzetten zonder dat eerst te repareren.fillColor op de twee aanmaakpagina's is al goed: zowel
stadsactiviteitAanmaken (18 widgets) als uitgaansevenementAanmaken (11)
staat volledig op primaryBackground. Het in CLAUDE.md voorspelde
secondaryBackground-defaultprobleem is dus niet meer aanwezig.$.categorie is op de Home-displays nooit leeg of afwezig (services_1/3/5,
Amsterdam, 25 items elk: altijd een lijst). De .toList() zonder true op
home_uitgaantabel_kaart_component_widget.dart:651 kan daardoor in de praktijk
niet crashen — events zonder categorie halen de displays sowieso niet.EvenementInfo (6 van de 12,
nagemeten en juist), de wees hierboven, en
HorecagelegenheidoverzichtKaart.nid (restant van het verwijderde hartje).node.title, een spatie komt
vóór de A). De eerste kaarten die een gebruiker ziet zijn dus willekeurig.android/ heeft geen ongecommitte
wijzigingen meer en de vastgelegde waarden zijn minSdkVersion
flutter.minSdkVersion (= 24) en tasks.register("clean", Delete) — precies
de moderne variant. Het punt is dus vanzelf beslecht; niemand hoeft hier nog
iets aan te doen.flutterflowmobiel1 opruimen (was taak 46): GESCHRAPT op Bob's verzoek.
services_2 bestaat nog (25 items) maar heet "ongebruikt" en blijft staan
tot een schoonmaakronde.| 09-14 | 09-19 | |
|---|---|---|
unieke events (flutterflow_events) |
16.065 | 17.123 |
| toekomstige events (op datum+tijd) | 6.202 | 6.684 |
| bereiken geen enkele app-tab | 622 (10 %) | 618 (9 %) |
| ... daarvan zonder categorie | 597 | 618 — dus 100 % |
| ... daarvan mét categorie | 25 | 0 |
De displays dekken alle categorieën nu volledig af — er is geen categorie meer die nergens landt. Het hele resterende lek is één oorzaak: een event zonder categorie valt door elk display-filter. Zie het blok bij taak 28/109; Bob vangt dat af bij de importer.
Ouderdomsverdeling (voor de opruim-cron van 180 dagen): 6.728 toekomstig · 8.107 jonger dan 180 dagen · 2.251 tussen 180 en 360 dagen · 14 ouder dan 360. Die 2.251 worden bij de eerstvolgende cronruns opgeruimd; met 1000 per run is dat ~3 dagen.
HTML-entiteiten: titels, adressen en de horeca-agenda zijn schoon (taak 129).
body heeft er nog 41 per 200 records, maar die gaat door de custom widget
FromHTML en rendert dus goed.
⚠️ Meetles: classificeer op datetime, niet op datum. Op datum alleen gaf
dezelfde meting 30 "bugs" die in werkelijkheid events van vandaag waren die al
begonnen waren — precies wat het >= -2 uur-filter hoort weg te laten.
flutterflow_events — bekend, geen impactNid 214439 ("Brocante Markt Klein Frankrijk") komt 14 keer identiek terug
in flutterflow_events; 214441 en 214420 elk 3x, 214367 2x. Op 2026-09-12
onveranderd (18 overtollige rijen op 2025).
✅ Maar het bereikt de app niet. Nagemeten 2026-09-12:
flutterflowmobiel1 heeft géén duplicaten: services_1/2/3/5/7 geven
allemaal evenveel rijen als unieke nids. Node 214439 komt er zelfs
helemaal niet in voor (zijn categorie "Tweedehands markt" valt onder geen
enkele display — dat is taak 28, niet deze).flutterflow_events wordt door de app alléén per nid bevraagd
(EvenementCall.call(nid: ...), vanuit evenement_component en
event_current). Er wordt nergens een lijst uit opgebouwd: de twee
iteraties in dat component lopen over fotoos en categories binnen één
event, niet over de rijen.Gevolg: geen zichtbare schade, geen haast. Het blijft wel iets om te weten:
bouwt er ooit iemand een lijst op deze view, dan komt zo'n node 14x in beeld.
De oorzaak is het klassieke Views-patroon "een veld met meerdere waarden krijgt
een eigen rij" — kijk op node 214439 welk veld 14 waarden heeft en zet
Multiple field settings → Display all values in the same row aan.
Gevraagd bij taak 28/109 ("bij een volgende contentmeting nakijken of het getal
daalt, nu 618"). Voor het eerst geclassificeerd op datum_iso in plaats van
op de Nederlandse datum-string — flutterflow_events levert dat veld sinds
2026-09-19 en is mét display_id=services_1 gewoon pagineerbaar (25/pagina).
| 09-12 | 09-14 | 09-19 | 09-20 | |
|---|---|---|---|---|
| unieke events | — | 16.065 | 17.123 | 16.080 |
| toekomstige events | — | 6.202 | 6.684 | 6.397 |
| bereiken geen enkele tab | 597 | 622 | 618 | 584 (9 %) |
| ... zonder categorie | 597 | 597 | 618 | 584 — 100 % |
| ... mét categorie | — | 25 | 0 | 0 |
Conclusie: 618 → 584, een daling van 34. Het liep eerder op (597 → 618), dus
de importer-aanpak van Bob lijkt te werken. De categorie-dekking van de zeven
displays blijft volledig: er is geen enkel toekomstig event dat wél een categorie
heeft en toch nergens landt. Voorbeelden van de rest: 222806 Steve 'n' Seagulls
(20 dec, Weert) · 222737 Altruism Label Night (21 nov, Maastricht) · 222703
BRECHTJE Maresa (1 okt, Groningen).
Twee bijvangsten:
datum_iso en vallen dus buiten élke
datumclassificatie. Klein, maar het betekent dat ze ook door het
>= -2 uur-filter van de master niet betrouwbaar te plaatsen zijn.services_2 geeft nu 0 rijen (stond in de notities nog op 25). Die display
staat al als "ongebruikt" te boek, dus geen impact — wel goed om te weten vóór
iemand 'm ooit wil hergebruiken.Alles wat buiten de app-code om stil kan zijn verlopen en een Play-inzending of de app zelf zou breken. Alles groen op één na (G3.1, zie daar).
| check | uitkomst |
|---|---|
/nl/support/privacybeleid (G2.1) |
200 anoniem |
/nl/support/mobieleapp (G2.2) |
200 anoniem |
/nl/account-verwijderen |
403 anoniem — daarom terecht NIET de data-deletion-URL |
deel-URL uitgk.com/<nid> |
200 op drie echte toekomstige events, landt op de juiste eventpagina |
| dichtgezet API-oppervlak | node · file · taxonomy_term · system/connect · user/<uid> allemaal 404 |
POST user/login.json met onzin |
401 (niet 404 — de route leeft dus nog) |
| de vijf app-views | alle 200 mét hun display_id |
plaatsen · plaatsen_bij_gemeente · horecacategorieen |
200 |
| Play-screenshots (10) | alle binnen 320-3840 px en ratio ≤ 2:1 |
| feature graphic | exact 1024×500, RGB zonder alpha, 88 kB |
⚠️ Meetval, kostte mij twee rondes: een views-endpoint zónder display_id
geeft 404 ["Display default on view X could not be found"]. Dat lijkt op een
dichtgezette resource maar is gewoon een onvolledige aanroep — roep een view
altijd aan zoals de app 'm aanroept. Idem flutterflow_events met het nid van
een horecagelegenheid: dat geeft terecht [], geen fout.
📌 uitgk.com/222338 uit de oude notitie klopt niet meer — die node bestaat
niet en de forward landt dan op de zaakpagina. Geen bug; test zoiets met een nid
uit een verse flutterflowmobiel1-aanroep.
Verse export + de patroonchecks uit CLAUDE.md. dart analyze op de verse
export: 0 errors. De twee gevonden punten zijn diezelfde dag opgelost en
export-geverifieerd:
Container om
Text-categorie in HorecagelegenheidEventTabelComponentCurrent stond op
Color(0xFF42E31C); nu op het thema-token Tertiary (#09B34A), gelijk aan
elk ander categorielabel. grep -rn "0xFF42E31C" lib/ geeft projectbreed 0.FilterBalkComponent zijn gevuld — Search by title · All · Today ·
Weekend · This week · initial option All. Nagemeten over alle 218
i18n-sleutels die in bereikbare widgets gerenderd worden: 0 met gevulde NL en
lege EN (was 6).
⚠️ Waarom "Weekend" tóch ingevuld moest: een lege en levert bij
getText() een lege string op, dus het chiplabel zou in het Engels
onzichtbaar zijn — EN==NL is hier geen reden om over te slaan.
✅ Het datumfilter blijft in het Engels werken: filterDatumVan/
filterDatumTot matchen op substring en kennen today al; This week bevat
week en All matcht niets (= geen filter). Dat is geverifieerd vóór het
vertalen, want de chip-labeltekst is tegelijk de waarde die in
FFAppState().datumFilter belandt.Blijft open (niet van Claude):
lib/ die de
verse export niet meer produceert. export-code schrijft alleen, verwijdert
nooit. Daardoor geeft dart analyze lokaal 2 errors (in
kanweg/kanweghorecagelegenheid_current_copy en shared/drawer_component_copy,
allebei Undefined name 'KanwegHorecagelegenhedenOverzichtWidget') terwijl de
verse export er 0 heeft. De build valt er niet over — niets importeert die
twee — maar het maakt de standaard "0 errors"-check onbetrouwbaar.
Advies: commit de huidige working tree niet as-is; draai een verse export en
commit die. Claude verwijdert niets — de mappen:
favorieten_copy · favorieten_copy2 · kanweg/ (8 bestanden) ·
kanweg_test_upload · shared/drawer_component_copy ·
shared/geen_evenementen_component · shared/header_current_copy ·
horecagelegenhedenoverzicht/horecagelegenheid_event_tabel_component_copy ·
uitgaanspaginas/home_copy2 · uitgaanspaginas/home_uitgaan_slider_component_copy ·
uitgaanspaginas/home_uitgaantabel_kaart_component_copy.Nagemeten en schoon — niet opnieuw doen:
! ?? in models (0) · getJsonField(...).toList() zonder true (0) ·
lege responsive-tak return 0; (0) · bron/pad-mismatch bij getJsonField (0) ·
ellipsis zonder maxLines (alleen in twee weeskomponenten) ·
ongelezen component-parameters (alleen de drie al bekende) ·
iOS UsageDescription staat op precies 3 · hardcoded kleuren in levende
widgets: alleen nog #09B34A/#EEEEEE (merkkleuren, niet getokeniseerd) en
transparante/schaduwwaarden.
showsTestAd: true staat nog aan — het restant van G0.6 (tel-04-horeca.png)
is dus nog steeds geblokkeerd op de AdMob-schakelaar.
Besloten 2026-09-21 na onderzoek. De site gebruikt al OneAll Social Login
(module 7.x-2.12, subdomein uitgaanskrantcom); 42 van de 298 accounts hebben
een koppeling (Facebook 29, Google 14, GitHub 3, Twitter/X 3, Microsoft 3). Nu
ook in de app.
Gekozen route: Direct Connect. Eén URL per netwerk, geopend in een Chrome
Custom Tab:
https://uitgaanskrantcom.api.oneall.com/socialize/connect/direct/facebook/?service=social_login&callback_uri=<urlencoded>
OneAll doet de OAuth en redirect naar de callback met ?connection_token=….
⚠️ Direct Connect zit niet in het Starter-plan (getest: "Direct Connect is
not available in the Starter plan") — vandaar de upgrade naar Personal
Advanced.
Twee dingen die dit ontwerp bepalen — niet omheen te bouwen:
disallowed_useragent), Facebook ook. Het moet een Custom Tab /
SFSafariViewController zijn: url_launcher met
LaunchMode.inAppBrowserView (6.3.1 staat al in pubspec) — niet
inAppWebView, dat is wél een WebView.sessid/session_name
als losse App State-waarden en plakt zelf Cookie: … op elke call. Een
browser-login levert dus niks op tenzij iets die sessie als waarde
teruggeeft. Dat is het hele bestaansrecht van het claim-endpoint hieronder.De keten: app maakt random state → opent Custom Tab → gebruiker logt in bij
Facebook → OneAll levert connection_token bij de callback → callback wisselt
'm in bij de OneAll API, logt in met user_login_finalize(), bewaart
sessid+session_name onder die state (cache, TTL 5 min, eenmalig) → app pollt
het claim-endpoint elke 2 s → krijgt de sessie → closeInAppWebView().
Bewuste keuze: pollen, geen deep link. Het schema
uitgaanskrant://uitgaanskrant.com bestaat en werkt, maar flutter_web_auth_2
vereist een eigen activity in AndroidManifest.xml en de export overschrijft
dat bestand. Pollen + closeInAppWebView() kost geen package, geen manifest en
geen go_router-route.
Voordeel dat je moet kennen: er hoeft géén aparte Facebook-app geregistreerd te worden. Alle OAuth-redirects lopen naar oneall.com; Facebook kent alleen OneAll als client. Er verandert dus niets aan de Facebook-instellingen van de site.
| # | Wie | Wat |
|---|---|---|
| 130-A | Bob | Upgrade naar Personal Advanced. ⚠️ "A security error has occurred" bij het upgraden = je zit op de billing-address-URL ZONDER parameters. De wizard maakt bij Upgrade to Advanced een order aan en draagt applicationid + application_orderid + oas mee in elke stap; het formulier post naar diezelfde URL. Ververs je die pagina of open je 'm los uit je geschiedenis, dan zijn die weg en faalt élke submit — herhaaldelijk, want je blijft op de kapotte pagina. Fix: opnieuw beginnen bij Change Plan (/applications/application/order/switch/plans/?applicationid=791421&subscription_planid=20) → Upgrade to Advanced = &action=switch_plan&subscription_planid=40&billing_period=annually (30 = Standard). Dan komt er een vers ordernummer en werkt het formulier gewoon. Niet de schuld van cookies of reCAPTCHA — beide nagemeten en in orde; de 503's op google-analytics/googletagmanager zijn Bob's DNS-sinkhole en raken de flow niet. Adres is leeg bij een upgrade (bewust, opnieuw bevestigen); kaart ...8826 staat al klaar. Huidig plan is Annual Starter ($116,16/jaar tot 13-06-2027), dus de overstap wordt verrekend |
| 130-B | ✅ Besloten 2026-09-21 | Automatisch account aanmaken. De callback maakt zelf een Drupal-account op het geverifieerde e-mailadres van het netwerk; zonder geverifieerd e-mailadres weigeren we. Gebruikersnaam afleiden en op uniciteit controleren. registration_method = manual in de module blijft ongemoeid — dat geldt alleen voor de website |
| 130-C | ✅ Besloten 2026-09-21 | Geen iOS op korte termijn → Apple-login overslaan. Play kent geen tegenhanger van richtlijn 4.8. De provider is een parameter in de URL, dus later bijzetten raakt de app-kant niet. Blijft open punt zodra iOS aan de beurt komt |
| 130-D | ✅ Code klaar (2026-09-21), Bob deployt | custom.social_login_app.inc — callback die de token inwisselt (hergebruikt social_login_core_get_settings() / _do_api_request() / _get_user_for_user_token() / _get_uid_for_email() / _map_identity_token_to_user_token()), inlogt met user_login_finalize() en de sessie onder de state in de cache-bin legt (TTL 5 min, eenmalig). php -l schoon op PHP 8.1. Staat in ~/tmp/ op aegir-o1 |
| 130-E | ✅ Code klaar (2026-09-21), Bob deployt | POST /flutterdrup/app_social_login/claim.json → {status: pending\|ok\|error}; bij ok staan sessid/session_name/token/user erin als bij user/login.json, maar met een klein user-object (uid/name/mail) — geen volledig object in een anoniem claimbaar antwoord. Access callback _custom_social_app_claim_access() (TRUE, huispatroon van custom_plaatsen_access) |
| 130-F | 🟡 Werkt op devbob (2026-09-21) | 4/4 tests groen: callback rendert, callback→claim vinden elkaar via de cache, een state is eenmalig, ongeldige state geeft 400. Nog te doen: de bijgewerkte .inc + de tweede hook_menu-regel (pad-variant) plaatsen, en daarna alles naar productie — de app praat met uitgaanskrant.com |
| 130-G | ✅ Code klaar (2026-09-21), nog niet in de builder | socialLoginStart(provider) — state genereren, Custom Tab openen (LaunchMode.inAppBrowserView, niet inAppWebView), elke 2 s claimen, App State vullen als drupalLogin, tab sluiten met closeInAppWebView(). Timeout 3 min. dart analyze 0 errors. Wacht op 130-F. ⚠️ Te verifiëren op een toestel: of Android de polling niet throttlet terwijl de Custom Tab voorgrond is — zo ja, claimt hij alsnog bij terugkeer, want de state is 5 min geldig |
| 130-H | 📋 Draaiboek klaar (2026-09-21) | Twee knoppen onder de bestaande Inloggen-knop, gemaakt door die knop te dupliceren (erft stijl). Facebook #1877F2/wit, Google wit met rand #DADCE0. Per knop een unieke Action Output Variable Name, anders dubbele declaratie in het model. Stappen in 130-app-draaiboek.md |
| 130-I | 📋 Teksten klaar (2026-09-21) | Log in with Facebook / Log in with Google, via het globe-icoontje. ⚠️ Een gedupliceerde knop erft de Engelse vertaling van het origineel, dus dit hoort bij 130-H, niet erna |
| 130-J | Bob | Echte doorloop met een Facebook- én een Google-account op een profile-build. Claude kan dit niet — geen wachtwoorden |
| 130-K | ✅ Tekst klaar (2026-09-21), Bob voert door | Drie wijzigingen in /nl/support/privacybeleid (sectie 1a account, sectie 3 derden met OneAll+Meta+Google, datum/draft) + in Data Safety komt Personal info → Name erbij als Collected. Volledige tekst in 130-K-privacy.md. ⚠️ Losse bug daarbij gevonden: de privacylink in de cookiebanner geeft 404 (/nl/support/algvoorwaarden, moet /nl/support/algemenevoorwaarden zijn) — Play eist een werkende privacy-URL, dus dit is sowieso repareren op /admin/config/system/eu-cookie-compliance |
| 130-L | Claude | Bevindingen naar CLAUDE.md |
⏰ HERINNERING VOOR 2026-09-22 (Bob vroeg hier expliciet om):
devbob.uitgaanskrant.com toevoegen aan de allowed domains in het
OneAll-dashboard. Kon op 2026-09-21 niet — devbob had een storing. Zonder dat
domein weigert OneAll de callback_uri en is testen op devbob onmogelijk.
Stand 2026-09-21, tweede testronde — backend is KLAAR:
Beide callback-routes bewezen op devbob: /callback?state=X én
/callback/X, en in beide gevallen vindt de claim de state terug
({"status":"error","message":"cancelled"}), geeft een tweede claim weer
pending en een te korte state een 400. Er valt aan de Drupal-kant niets meer
te testen tot het OneAll-abonnement omgaat. Rest: de aangepaste .inc +
het tweede hook_menu-item nog naar productie (de oude versie staat er al).
Stand 2026-09-21 na de eerste testronde:
uitgaanskrantcom) als
productie. Testen op devbob kan dus, maar dan moet devbob.uitgaanskrant.com
bij de allowed domains staan (Advanced geeft er 8)./nl/app-social-login/callback/<state>). Het is niet bewezen dat OneAll een
bestaande query-parameter in de callback_uri laat staan als hij er zelf
?connection_token= aan toevoegt; met de state in het pad kan dat niet
misgaan. De query-variant blijft bestaan als terugval en om handmatig te
testen — daarom staan er twee hook_menu-items.Alles wat zonder het abonnement kon, is af (2026-09-21). Backend getest, app-code geschreven, privacytekst klaar, bouwdraaiboek klaar. Openstaand tot de upgrade: alleen 130-A, en daarna G t/m J in één sessie.
⚠️ Besluit Bob 2026-09-21: Claude werkt pas in FlutterFlow zodra het abonnement is aangepast. Tot die tijd alleen voorbereiden.
💡 De app-keten is te testen zonder de upgrade: vervang in
socialLoginStart de startUrl tijdelijk door callbackUri, dan slaat hij
OneAll over en loopt de rest van de keten wél (browser, polling, foutafhandeling,
tab sluiten). Zie het draaiboek.
Volgorde: A t/m C zijn besluiten en blokkeren D. F blokkeert G. K moet af vóór de Play-inzending als 130 in de eerste release meegaat.