TASKS.md 108 KB

TASKS — alleen nog OPEN taken

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


📍 Waar staat het project?

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.

👤 CLAUDE — OPEN

# Taak Kort
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

| 112 | Restje zoek-stad: lege API-call opruimen | Eigenaar: Bob. SelectStateDropDownComponent → Actions → On Page Load → Conditional Action 5 (gemeenteSelectId is set/non-empty) → TRUE-tak → Action 13, Custom Action drupalRequest met url/sessionName/sessionId/token/body allemaal Unset. Staat sinds 2026-09-20 op disabled; kan weg via ⋮ → Delete Action. Vlak eronder staat Conditional Action 16 (apiGemeenteResultInitalize -> Succeeded) met twee lege takken — mag in dezelfde moeite weg |

| 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 | | 128-B/C/D | Nieuwsbrieven-blok op mijnProfiel | Backend staat live en is getest; klaar om te bouwen zodra de lopende FlutterFlow-sessie klaar is (Bob meldt dat). Blok + waarschuwing bij 0 gevolgde gemeenten + vertalingen. ⚠️ CSRF-token meegeven, zie het blok hieronder |

👤 BOB — OPEN, in volgorde van wat de livegang blokkeert

# 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
115 AdBanner verplaatsen AF (Bob 2026-09-20, export-geverifieerd): volgorde is nu slider → banner → filterbalk → lijst
116 AdBanner springt 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
121 15 verweesde mappen in lib/ opruimen Staan lokaal én in git, maar FlutterFlow levert ze niet meer. Twee ervan breken dart analyze. Eigen blok hieronder
120 12 hardcoded kleuren aan thematokens binden ⚠️ Claude kwam er 2026-09-20 niet in: widget-selectie in de tree landt niet in Bob's huidige Chrome-venster. Inventaris mét vindplaatsen staat klaar
117 Kalenderpermissie eruit AF (Bob 2026-09-20, export-geverifieerd): READ_CALENDAR/WRITE_CALENDAR én NSCalendarsUsageDescription zijn weg; CAMERA staat er nog (terecht)
118 AdMob EU-consentbericht ⚠️ Half: de FlutterFlow-toggle staat goed, maar het bericht moet in de AdMob-console gepubliceerd zijn. Eigen blok hieronder
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
127 Kopie-knop op MijnAanmeldingen (clonen) Wens Bob 2026-09-20, vóór de livegang. Backend bestaat al; Drupal-patch voor 3 gaten staat klaar en is getest (127-A). App-deel in stappen B t/m F; het activiteitendeel (127-E) deelt de cascade met 124. Eigen blok hieronder
128-A KLAAR — nieuwsbrieven-resource staat live Gedeployd en getest op productie 2026-09-21. Rest: één tikfout in de omschrijving van tid 36676 (verstuurt → verstuurd), zie het blok hieronder
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

#

🔧 Kleine open restpunten (app)

(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 MijnProfielBekijk alles. Dat volstaat; alleen oppakken als je 'm ook direct vanaf Favorieten wilt kunnen bereiken.


📰 128 · Nieuwsbrieven aan/uit in de app · Eigenaar: Bob (A) + Claude (B-D)

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.

Wat er al staat — gemeten op productie

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.

⚠️ De bevestigingsmail — gemeten, want hier zit een valkuil

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.

Eerder als risico genoteerd, nu verklaard

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.

128-A · Drupal: resource 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.

⚠️ CSRF-token is VERPLICHT bij POST — cruciaal voor 128-B

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.

Kleine restjes op 128-A

  • unsubscribe.json is nog niet één keer aangeroepen. Zelfde route als subscribe, dus laag risico; de app-test dekt het vanzelf af.
  • Tikfout in de omschrijving van tid 36676, die letterlijk in de app komt: *"Deze wordt donderdagavond verstuurt"* → verstuurd. Te wijzigen op /admin/config/services/simplenews (Description van de nieuwsbrief).
  • Alles met 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.

128-B · App: vierde tab "Nieuwsbrieven" op mijnProfiel · Eigenaar: Claude

Bob 2026-09-21: het wordt een losse TAB op de profielpagina, geen blok onderaan. Dat is gunstig, want mijnProfiel heeft al een TabBar met drie tabs — Mijn evenementen, Stadseditor, Account (controller tabBarmijnevenementenController, TabBar op regel 188 van de verse export). Een vierde toevoegen gaat via het bekende recept: TabBar-node selecteren → "Active Tab"-dropdown → onderaan "+ Add Tab" → hernoemen naar Nieuwsbrieven. Dat voegt meteen een bijpassende lege TabBar Page toe.

In die tab:

  1. API Call Nieuwsbrieven (GET /nl/flutterdrup/nieuwsbrieven.json) met de Cookie-header in het [session_name]=[sessid]-formaat — rechte haken, geen {{ }} — en de aanroeper moet sessionName/sessid uit App State meegeven, anders gaat er Cookie: = mee.
  2. Backend Query + Generate Dynamic Children. Bind via Predefined Path ($[:], Is List) en niet via JSON Path $, anders crasht de tab bij een mislukte call.
  3. Per rij: naam + omschrijving eronder (bodySmall / secondaryText), rechts een Switch met Initial Value op $.geabonneerd.
  4. On Toggle → drupalRequest naar nieuwsbrieven/subscribe.json resp. unsubscribe.json, body {"tid": <$.tid>}.

⚠️ Geef de CSRF-token mee (zie het blok bij 128-A): exact dezelfde vijf argumenten als het favorieten-hartje, inclusief FFAppState().userToken. Vergeet je die, dan komt er ["CSRF validation failed"] terug, geeft drupalRequest stil [] en lijkt de switch te werken terwijl er niets gebeurt.

⚠️ Doe "Test API Call" met een gevulde respons vóór je $.status/$.geabonneerd bindt, anders krijgt het pad type "Anything" en plakt de builder er .toString() achter.

Bij status == 2 een regel "Check je mail om je inschrijving te bevestigen" tonen. Die stand komt met de huidige instelling nooit voor (ingelogde gebruikers worden direct verwerkt), maar dan is de app al klaar als dat ooit verandert.

128-C · Waarschuwing bij 0 gevolgde gemeenten · Eigenaar: Claude

Bob's keuze 2026-09-21. Gemeten: 17 van de 18 actieve abonnees op de wekelijkse agenda hebben nul favoriete gemeenten, en de laatste editie (nid 222817, 2026-09-17) ging naar sent_subscriber_count: 3. Wie die switch aanzet zonder gemeenten te volgen, krijgt dus niets — zonder enige uitleg.

Bestemming van de knop is uitgezocht: pagina selectprovinciegemeente (/selectprovinciegemeente) — die heeft geen parameters, dus de actie is één kale Navigate To zonder bindingen.

Onder de rij van tid 36676 een waarschuwing tonen wanneer FFAppState().favorieteGemeenteIds leeg is, met een knop naar het gemeente-keuzescherm. Visibility via de bekende route: App State-lijst -> Available Options Is Set and Not Empty + Apply Opposite Statement. De conditie op de tid zelf hoort een bool custom function te zijn (geen JSON-Path-binding) — anders komt er if (getJsonField(...)) uit, een dynamic als bool zonder null-check.

(Niet nagelezen: de scs-contentselectie zelf. De tabel heet niet scs_node; ik heb dus niet hard bewezen dát die 17 een lege mail krijgen — het beeld past wel. Wie dit ooit exact wil weten, begint bij node 189237, de scheduler-parent.)

128-D · Vertalingen · Eigenaar: Claude

Kop + waarschuwingstekst + "check je mail"-regel via het globe-icoontje. ⚠️ De omschrijvingen komen uit Drupal en krijgen dus géén globe en geen EN-vertaling. Die van 36676 bevat bovendien een tikfout die Bob in Drupal kan meenemen: *"Deze wordt donderdagavond verstuurt"* -> verstuurd.

Bevinding terzijde — geen taak

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.

🔔 129 · Sessie-verlopen-melding — verifiëren · Eigenaar: Bob (andere chat)

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.

⚠️ 86 bestanden verschil tussen de gecommitte lib/ en de builder

Gemeten 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

  • commit waarschijnlijk op zijn plaats — dan loopt git weer gelijk met de builder en kloppen dit soort metingen weer.

🔭 VERSIE 2.0 — na de livegang · niet agenderen vóór release

Ideeën die af zijn onderzocht maar bewust wachten. Bob 2026-09-20: eerst live, dan pas kijken of dit überhaupt de moeite is.

2.0-A · App Links: uitgaanskrant.com-links in de app openen

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:

  1. De site-URL's zijn SEO-aliassen zonder nid en zonder type (/nl/Noord-Holland/Amsterdam/Bimhuis-Amsterdam). De app kan daar niets mee zonder Drupal om een vertaling te vragen — een extra endpoint erbij.
  2. Je zou élke link naar je site de app in trekken: privacybeleid, registratieformulier, zoekpagina's, de hele CMS-kant. Voor alles wat de app niet kent moet je dan een nette terugval bouwen.

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:

  • A — deel-URL kort houden (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.
  • B — deel-URL wordt 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.

2.0-B · Tab-fetches cachen op het horeca-overzicht

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.

🅖 G · GOOGLE PLAY — volledige checklist · Eigenaar: Bob

*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:

  • Een organisatie-account is vrijgesteld van de 12-testers/14-dagen-regel. Die geldt alleen voor persoonlijke accounts van ná 13-11-2023. De keuze voor Organisatie was dus juist en scheelt minimaal twee weken.
  • D-U-N-S duurt tot 30 dagen. Dat is de kritieke pad-stap, niet de $25.
  • Legal name + adres in het Google Payments-profiel moeten EXACT matchen met het D-U-N-S-profiel. Mismatch = e-mail met deadline, daarna verwijdering van de app uit Play. Dit is de meest gemaakte fout en kost weken.

#

G0 · Nu doen, terwijl de verificatie loopt (buiten Play Console)

  • G0.1 — D-U-N-S: AL BINNEN (Bob 2026-09-19: "die krijg je standaard in Nederland, heb ik al gevonden"). De langste wachtstap valt dus weg. ⚠️ Blijft staan: leg het nummer vast mét de exact daarop geregistreerde bedrijfsnaam en adres — die moeten letterlijk matchen met G0.2.
  • G0.2 — Payments-profiel gelijktrekken met precies die naam/adres.
  • G0.3 — ✅ BESLOTEN (Bob, 2026-09-19): alles via FlutterFlow Deploy. Lokaal bouwen is afgevallen: het heeft twee stille faalmodi die allebei al zijn opgetreden (Flutter-vogeltje als icoon zonder 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.
  • G0.4 — ✅ BESLOTEN (Bob, 2026-09-19): alleen Android, iOS later. Bob 2026-09-19, definitief: "fuck ios. we gaan eerst een android app bouwen. Als die geld oplevert, gaan we naar apple." Niet opnieuw agenderen; wat het zou kosten staat in het blok "iOS — wat er extra bij komt".
  • G0.5 — ✅ CONCEPT KLAAR (Claude, 2026-09-19), akkoord Bob. Drie varianten korte omschrijving (70/75/71 tekens) + lange omschrijving (1585 van 4000), opgebouwd uit de echte functielijst (drawer + Home-tabs). Bob's aanvullingen verwerkt: account is gratis, geen in-app aankopen; de formulering "horecaondernemers en stadsredacteuren" blijft staan. 📁 Tekst staat in ~/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).
  • G0.6 — ✅ NIEUWE SET KLAAR (Claude, 2026-09-19 avond). Alle tien opnieuw geschoten uit een verse --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.
  • G0.7 — ✅ KLAAR (Claude 2026-09-20, variant B gekozen door Bob). 📁 ~/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.

#

G1 · Zodra het account geverifieerd is

  • G1.1 — Create app. Naam "Uitgaanskrant.com", Nederlands, App (geen game), Gratis. Package is com.uitgaanskrant.apponveranderlijk na de eerste upload, dus controleer 'm.
  • G1.2 — Payments-profiel verifiëren (deposit-challenge of bankdocument, ±5 dagen). Moet vóór publicatie rond zijn.

#

G2 · App content (Policy and programs → App content)

  • G2.1 — Privacy policy-URL: https://uitgaanskrant.com/nl/support/privacybeleid (anoniem 200, gemeten 2026-09-16).
  • G2.2 — Data deletion-URL: 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.
  • G2.3 — App access: "All or some functionality is restricted" + de inloggegevens van het testaccount (taak 86, al aangemaakt). ⚠️ Wachtwoord niet in dit bestand — alleen Play Console + wachtwoordmanager. Zorg dat er 3-4 favorieten in Amsterdam en één gevolgde gemeente op staan, anders ziet de reviewer vier lege tabs en leest dat als kapot.
  • G2.4 — Ads: Contains adsJA (AdMob zit in de build).
  • G2.5 — Content rating: IARC-vragenlijst invullen.
  • G2.6 — Target audience: bewust kiezen — er staat 18+ uitgaanscontent in.
  • G2.7 — Data safety. Gemeten in het manifest van de release-APK: permissies 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.
  • G2.8 — Government apps / Financial features: beide nee (staat er wel, makkelijk over te slaan, blokkeert anders de release).

#

G3 · Store listing

  • G3.1 — App-icoon 512×512. ⚠️ 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.
  • G3.2 — Feature graphic 1024×500 (uit G0.7).
  • G3.3 — Screenshots telefoon + 7" + 10" tablet (uit G0.6). Tabletshots zijn verplicht omdat de app tablets ondersteunt.
  • G3.4 — Korte + lange omschrijving (uit G0.5).
  • G3.5 — Categorie + contactgegevens + e-mail. Die e-mail wordt publiek getoond.

#

G4 · Release

  • G4.1 — versionCode ophogen vóór élke upload. Staat nu op 1.0.0+1 (pubspec.yaml:18). Bij de FlutterFlow-route: App Settings → App Details.
  • G4.2 — Build uploaden volgens de route uit G0.3. AAB, niet APK — die splitst per architectuur (de APK is nu 95,6 MB).
  • G4.3 — ⚠️ Icoon controleren op het geüploade artefact zelf. Open vraag: doet FlutterFlow's deploy-pipeline flutter_launcher_icons wél? Lokaal gebouwd draagt het bestand anders het Flutter-vogeltje. Niet aannemen.
  • G4.4 — Interne test eerst (geen tester-minimum voor organisaties), dan pas productie.

#

🍏 iOS — wat er extra bij komt (Bob's vraag 2026-09-19; besluit: NIET nu)

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:

  • D-U-N-S-nummer — Apple eist dat voor een organisatie-account, net als Google. Dat is de langste stap en die is bij jou al gezet.
  • De permissieteksten in Info.plist staan al goed (Camera, PhotoLibrary, Calendar) — geverifieerd 2026-09-19, precies 3 keys.

Wat er nieuw bij komt:

  1. Apple Developer Program-account (~$99/jaar) + verificatie van de organisatie.
  2. Bundle ID + certificaten/provisioning in App Store Connect. Er is nu niets voor iOS ingericht: alleen ios/Runner.xcodeproj/project.pbxproj beweegt mee in elke export.
  3. Eigen screenshots in Apple-formaten (iPhone-maten + iPad) — andere afmetingen dan Play, dus die van G0.6 zijn niet herbruikbaar.
  4. Privacy Nutrition Labels — Apple's tegenhanger van Data Safety, apart invullen.
  5. ⚠️ App Tracking Transparency (ATT). AdMob gebruikt op iOS de IDFA. Wil je gepersonaliseerde advertenties, dan moet er een ATT-prompt komen bovenop de bestaande GDPR-consentflow. Dat is echt extra bouwwerk, geen vinkje.
  6. ⚠️ Guideline 5.1.1(v): account verwijderen moet IN de app kunnen. Onze knop (taak 85) opent een webpagina. Voor Play is dat prima; Apple is daar strenger over en dit is een klassieke reden voor een afwijzing. Reken op een echte in-app verwijderroute vóór een iOS-inzending.
  7. Guideline 4.2 (minimum functionality) — apps die vooral webcontent tonen worden geweigerd. Deze app heeft genoeg eigens (favorieten, aanmelden, agenda-integratie, offline-melding), dus dat risico is klein, maar het is wel waar Apple naar kijkt.

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.

115 · ✅ AdBanner verplaatst — AF (2026-09-20)

Verse export bevestigt de body-Column van PUitgaanPage: [slider (if !zoekOpen), Container > AdBanner, Expanded > FilterBalkComponent, Expanded > lijst]. Precies de gevraagde volgorde.

116 · ✅ AdBanner-hoogte — AF (2026-09-20)

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.

122 · Teksten aanmaakpagina's — A/B/C AF, D/E/F open · Eigenaar: Bob

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:

  • D · hints die alleen het label herhalen. Elk veld heeft een 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".
  • E · resterend verschil tussen de pagina's. Alleen nog "Omschrijving …": ACT EN Description activity tegenover EVT EN Event description.
  • F · Engelse placeholders op een Nederlandse pagina. 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.
  • ⚠️ Drie woorden voor hetzelfde ding op 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.

123 · Stadsrechten-cascade op stadsactiviteitAanmaken — Drupal-deel · Eigenaar: Bob

Code staat klaar en is getest (2026-09-20, Claude): vervang in sites/all/modules/custom/custom.evenementen_aanmaken.inc de regels 316 t/m 372 (docblock t/m sluitende accolade van _custom_mijn_stadsrechten_service()) door de aangeleverde versie. Daarna drush @<alias> 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\|\|

124 · Stadsrechten-cascade — app-deel · Eigenaar: nader te bepalen (na 123)

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 / ketenPlaatssplit('|')[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.

125 · Crash: lege plaatsenlijst sloopt de Plaats-dropdown · Eigenaar: nader te bepalen

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: Generate-Dynamic-Children-achtige null-veiligheid via een Predefined Path op de API-call ($[:], Is List) i.p.v. een JSON Path — dat genereert (XCall.items(jsonBody)?.toList() ?? []). Recept staat in CLAUDE.md.

126 · Stale createPlaatsID bij het wisselen van gemeente · Eigenaar: nader te bepalen

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

127 · "Kopie"-knop op MijnAanmeldingen — evenement én activiteit klonen

Eigenaar: Claude — bezig (sessie 2026-09-21).

AF en export-geverifieerd:

  • 127-A (Drupal, door Bob): entree_tid 36197, logo_url gevuld, keten 28666|28737|28739.
  • 127-B (infra): App State kloonBron (JSON, niet persisted); custom functions kloonTekst / kloonLijst, beide met een non-nullable return; API Call AanmeldingKloonbron (top-level, GET, absolute URL) met 'Cookie': '${sessionName}=${sessid}' — echte interpolatie.
  • 127-C (de knop): Kopie op de MijnAanmeldingen-kaart, buiten de bestaande InkWell (de kaart-Column is gewrapt in een Column). Volledige keten staat en dart analyze geeft 0 errors:
_model.apiResulttzz = await AanmeldingKloonbronCall.call(
  sessionName: FFAppState().userSessionname,
  sessid: FFAppState().userSessionid,
  nid: getJsonField(aanmeldingItemItem, r'$.nid').toString(),
);
if ((_model.apiResulttzz?.succeeded ?? true)) {
  FFAppState().kloonBron = (_model.apiResulttzz?.jsonBody ?? '');
  if (widget!.type == 'go_out_event') {
    context.pushNamed(UitgaansevenementAanmakenWidget.routeName,
      queryParameters: {'horecagelegenheidNid': ... r'$.horecagelegenheidnid' ...});
  } else {
    context.pushNamed(StadsactiviteitAanmakenWidget.routeName);
  }
}

ℹ️ FlutterFlow genereert bij een Backend Call zelf een apiResulttzz -> Succeeded-conditional; die is blijven staan, dus er wordt alleen genavigeerd als de call slaagt. Meegenomen winst, niet apart gebouwd.

127-C is OP HET TOESTEL BEWEZEN (2026-09-21, profile-build x86_64 op emulator-5554, ingelogd als bobcity). Beide takken:

  • Evenement (?type=go_out_event) → opent uitgaansevenementAanmaken mét Je horecagelegenheid al op "Café de Vriendschap" — dus de page parameter komt door. De rest is nog leeg, want 127-D moet nog.
  • Activiteit (?type=activity, testnode 222818) → opent stadsactiviteitAanmaken, niet het evenementformulier. De type-conditie klopt.
  • 0 exceptions in logcat bij beide.

Omdat er alleen genavigeerd wordt als de call slaagde, bewijst de navigatie tegelijk dat de sessie-cookie werkt en kloonBron gevuld is.

De i18n-sleutel g4pbs1et is vertaald (nl: Kopie / en: Copy).

Eén restpunt op 127-C: de knop is nog FlutterFlow-rood (primary). Voorstel: secondaryBackground + primaryText met Border Radius 8, zoals de chips op Home — rood is merk, niet actie (zie het merk-DNA in CLAUDE.md). Het Fill Color-veld valt onder de vensterrand; klap eerst Button Text, Alignment en Padding in (één klik per tool-aanroep).

Nog te doen: 127-D (evenementformulier, 14 bindingen), 127-E (activiteiten, na 124), 127-F (kloonBron wissen op 4 plekken).

Wens Bob (2026-09-20): op elke kaart in Mijn aanmeldingen een knop Kopie. Eén tik opent het aanmaakformulier volledig ingevuld; de gebruiker vult de datum aan, past eventueel iets aan en dient in. Vóór de livegang. Beide types.

De datum komt bewust NIET mee — je kloont juist om hetzelfde op een nieuwe datum te zetten. Dat zit al zo in de kloonbron.

Wat er al is (gemeten 2026-09-20, niet opnieuw uitzoeken)

GET /nl/flutterdrup/aanmelding_kloonbron.json?nid=X bestaat en staat aan op productie (custom.mijn_aanmeldingen.inc:248, resource aanmelding_kloonbron, operation index). Sessie-geauthenticeerd, met eigenaarscheck (403 op andermans node, 404 onbekend, 400 bij een ander contenttype). De sleutelnamen zijn met opzet gelijk aan wat evenementen/create en stadsactiviteiten/create verwachten — tids en fids, geen weergavewaarden.

Aan de app-kant is nog niets: geen API Call, geen knop, 0 treffers op kloonbron in een verse export.

127-A · Drupal: drie gaten in de kloonbron dichten · Eigenaar: Bob

Code staat klaar en is op productie getest (2026-09-20, Claude, read-only via drush php-script — het bestand is niet gewijzigd). Drie gaten, alle drie hard gemeten:

# Gat Bewijs
1 entree_tid ontbreekt bij een activiteit De veldkaart heeft 'entree' => NULL voor activity, maar _custom_stadsactiviteit_create() schrijft wél naar field_act_entree (Bob 2026-08-26, zelfde vocabulary als field_goo_entree). Testnode 222818 heeft field_act_entree = 36197; de kloonbron gaf dat niet terug → dataverlies bij klonen
2 Geen logo_url/fotos_urls De aanmaakpagina toont de preview uit page state createLogoUrl/createFotosUrls. Zonder URL's gaat het beeld wél mee (de fid) maar zie je een leeg previewvak — verwarrend
3 Geen plaatsketen bij een activiteit Alleen plaats_tid. De pagina heeft drie cascade-dropdowns (provincie → gemeente → plaats) en kan uit één tid niet afleiden welke provincie/gemeente erbij horen

De patch staat als kloonbron-patch.inc in het sessie-scratchpad van 2026-09-20; opnieuw te genereren met de beschrijving hieronder. Twee ingrepen in sites/all/modules/custom/custom.mijn_aanmeldingen.inc:

  1. Regel 125: 'entree' => NULL,'entree' => 'field_act_entree',
  2. Regels 240 t/m 313 (docblock t/m sluitende accolade van _custom_aanmelding_kloonbron()) vervangen door de nieuwe versie. Die voegt twee helpers toe (_custom_aanm_bestand_urls() en _custom_aanm_keten()) en geeft extra terug: logo_url, fotos_urls, entree_tid (nu voor beide types, uit het gedeelde blok), en voor een activiteit provincie_tid, gemeente_tid en keten.

Daarna drush @uitgaanskrant.com cc all.

⚠️ keten heeft bewust exact hetzelfde formaat als taak 123 ("provincie|gemeente|plaats", ontbrekende delen leeg). De drie custom functions uit taak 124 (ketenProvincie / ketenGemeente / ketenPlaats) zijn daardoor 1-op-1 herbruikbaar — bouw ze niet twee keer.

Getest op productie, beide types plus alle randgevallen:

invoer keten
plaats 28739 (Hoogkarspel) 28666\|28737\|28739
gemeente 28694 (Amsterdam) 28666\|28694\|
provincie 28666 (Noord-Holland) 28666\|\|
NULL / 0 / onbestaande tid \|\| (geen crash)

Gemeten: alle 23 gevulde field_act_state_municipality_tow-waarden staan op niveau 3 (plaats). Er is dus geen activiteit waarbij plaats_tid en het derde deel van de keten uiteenlopen — dat randgeval hoeft niet afgevangen te worden.

ℹ️ Niet "repareren": file_create_url() geeft onder drush http://. Dat is een drush-artefact (geen request-context, $base_url staat op http://uitgaanskrant.com). Via een echte HTTP-request levert Drupal gewoon https:// — geverifieerd op flutterflowmobiel_establishment_info. Controleer het bij de eerste test vanuit de app, maar ga er niet aan sleutelen.

127-B · App: transport via App State · Eigenaar: nader te bepalen

Waarom niet gewoon On Page Load? Een TextField leest zijn Initial Value tijdens de eerste build; een Backend Call op On Page Load draait in een addPostFrameCallback, dus daarná. Het veld blijft dan leeg terwijl de data binnen is. Zelfde valkuil als bij Initial Option Value (staat in CLAUDE.md). Daarom: de call gebeurt op de knop, vóór de navigatie.

Besluit Bob 2026-09-20: App State, niet page parameters. Eén JSON-veld scheelt de helft van het bindwerk (1 binding bij het vullen i.p.v. 16) en houdt de route schoon.

Nieuw aan te maken:

wat waar details
App State kloonBron App Values → State Type JSON, Is List uit, niet persisted
API Call AanmeldingKloonbron API Calls GET https://uitgaanskrant.com/nl/flutterdrup/aanmelding_kloonbron.json, header Cookie: [sessionName]=[sessid] (rechte haken!), query nid. Kopieer MijnAanmeldingenCall als voorbeeld
Custom function kloonTekst Custom Code String kloonTekst(dynamic bron, String sleutel)non-nullable return, '' bij ontbreken
Custom function kloonLijst Custom Code List<String> kloonLijst(dynamic bron, String sleutel) — non-nullable, [] bij ontbreken
// kloonTekst — plat schrijven, geen closures (FlutterFlow's validator
// struikelt over een genest (params) { ... }-patroon).
String kloonTekst(dynamic bron, String sleutel) {
  if (bron == null || bron is! Map) return '';
  final waarde = bron[sleutel];
  if (waarde == null) return '';
  return waarde.toString();
}

List<String> kloonLijst(dynamic bron, String sleutel) {
  final uit = <String>[];
  if (bron == null || bron is! Map) return uit;
  final waarde = bron[sleutel];
  if (waarde is List) {
    for (final item in waarde) {
      if (item != null) uit.add(item.toString());
    }
  }
  return uit;
}

⚠️ Return type non-nullable houden. Bij een String? eist FlutterFlow een "Default Variable Value", en valueOrDefault substitueert die óók bij een lege string — dan kun je nooit een leeg veld klonen. Geen getJsonField() gebruiken: die bestaat niet binnen een Custom Function (wel in een Custom Action).

127-C · De knop op de kaart · Eigenaar: nader te bepalen

Huidige opbouw van de kaart (mijn_aanmeldingen_widget.dart, itemBuilder): Padding > Container > Padding > InkWell > Column [logo, titel, datum_start, status_label]. De hele kaart is één InkWell.

De knop moet buiten die InkWell staan, anders vuurt ook de navigatie naar EventCurrent. Recept (staat in CLAUDE.md): rechtsklik de InkWellWrap Widget → Column → "+" op die nieuwe Column → de knop. Een TabBar Page of InkWell accepteert maar één kind, een Column meerdere.

Actieketen op de knop:

  1. Backend CallAanmeldingKloonbron, nid = $.nid van het loop-item (aanmeldingItemItem), sessie uit App State.
  2. Update App StatekloonBron = Action Outputs → JSON Body → No Further Changes. Doe dit in de Action Flow Editor (knop Edit, dan het |< om de trigger-kolom in te klappen) — in het compacte Actions-paneel reageert het Value-veld niet.
  3. Add Conditional op de page parameter type (Equal to go_out_event):
    • TRUE → Navigate To uitgaansevenementAanmaken, page parameter horecagelegenheidNid = $.horecagelegenheid_nid uit de action output.
    • FALSE → Navigate To stadsactiviteitAanmaken.

⚠️ Conditioneer op de page parameter, niet op $.type van het item: een JSON-Path-binding krijgt type Json en levert if (getJsonField(...)) op — een dynamic als bool, die compileert en pas bij runtime crasht. De page parameter is een gewone String, dus een veilige Single Condition.

Gratis meegenomen: horecagelegenheidNid bestaat al als page parameter en heeft in horecaVoorselectie() (custom_functions.dart:245) al voorrang op de automatische voorselectie. Voor evenementen hoeft de horecagelegenheid dus niet via App State.

127-D · Evenementenformulier voorvullen · Eigenaar: nader te bepalen

14 bindingen. Initial Value = kloonTekst(FFAppState().kloonBron, '<sleutel>').

kloonbron-sleutel doel soort
titel textFieldTitel Initial Value
omschrijving textFieldOmschrijving Initial Value
entree_prijs textFieldEntreeprijs Initial Value
toelichting_entree textFieldToelichtingEntree Initial Value
website_url textFieldWebsiteURL Initial Value
tickets_url textFieldTicketsURL Initial Value
entree_tid dropDownEntree Initial Option Value + page state createEntreeTid in On Page Load
categorie_tids dropDownCategorieen Initial Option Value (kloonLijst) + page state createCategorieTids
logo_fid / logo_url page states createLogoFid / createLogoUrl
fotos_fids / fotos_urls page states createFotosFids / createFotosUrls
horecagelegenheid_nid page parameter ✅ gratis, zie 127-C
datums bewust leeg

Waarom een dropdown twee bindingen nodig heeft: de page state wordt gezet in onChanged, en die vuurt niet bij een voorselectie. Zonder de tweede binding selecteert de dropdown wél de juiste waarde maar verstuurt de create-actie leeg.

De page states worden pas bij het indienen gelezen, dus die mogen gewoon in On Page Load — daar speelt het timingprobleem niet.

127-E · Activiteitenformulier voorvullen · ⚠️ na 124 · Eigenaar: n.t.b.

24 bindingen. Tien TextFields (titel, omschrijving, organisator, contact, adres_straatAdres, adres_plaatsPlaats, adres_postcode, entree_prijs, toelichting_entree, website_url), plus entree_tid en categorie_tids (elk 2), de vier beeld-page-states, en de cascade (3 × 2).

De cascade is precies taak 124. Beide moeten de drie dropdowns voorvullen uit een keten-string, met dezelfde drie custom functions. Bouw 124 eerst, of bouw ze samen — anders doe je het werk twee keer.

Hoe de timing hier uitpakt (gemeten in de export):

  • De gemeente-dropdown zit achter if (createProvincieID != null && != '') (stadsactiviteit_aanmaken_widget.dart:1852) en de plaats-dropdown achter if (createGemeenteID ...) (regel 1991). Die twee bestaan nog niet tijdens de eerste build, dus hun FormFieldController wordt pas ná On Page Load aangemaakt — daar is het eerste-build-probleem er dus niet.
  • Alleen de provincie-dropdown staat er meteen. Die ene heeft zijn Initial Option Value uit App State nodig: ketenProvincie(kloonTekst(kloonBron,'keten')).
  • On Page Load moet alle drie de page states zetten (createProvincieID/createGemeenteID/createPlaatsID), want een voorgeselecteerde dropdown vuurt geen onChanged.

ℹ️ gemeenteTid bestaat al als page parameter van deze pagina en heeft voorrang in gemeenteVoorselectie() — bruikbaar, maar hij zet alleen createGemeenteID en niet createProvincieID, dus de gemeente-dropdown blijft dan onzichtbaar. Voor een kloon is de App-State-route via keten dus de volledige oplossing.

127-F · kloonBron wissen · Eigenaar: nader te bepalen

Zonder dit blijft een kloon hangen: wie daarna een vers evenement aanmaakt, krijgt het vorige formulier opnieuw ingevuld.

Zet vóór elke bestaande "nieuw aanmaken"-navigatie een Update App State met kloonBron op Clear Value (zelfde patroon als de uitlogknop). Vier plekken in een verse export van 2026-09-20:

bestand regel doel
mijn_profiel_widget.dart 823 UitgaansevenementAanmaken
mijn_profiel_widget.dart 1317 StadsactiviteitAanmaken
mijn_profiel_widget.dart 1506 StadsactiviteitAanmaken
favorieten_widget.dart 1042 StadsactiviteitAanmaken

Dit zijn alle ingangen: de aanmaakpagina's zijn verder nergens vanuit de app bereikbaar (wel via een directe route).

Volgorde en omvang

  1. 127-A (Drupal) — code staat klaar, ±15 min, blokkeert alles.
  2. 127-B (App State + call + 2 functies) — ±30 min.
  3. 127-C + 127-D + 127-F (evenementen, end-to-end werkend) — één sessie.
  4. 124/125/126 (cascade) en daarna 127-E (activiteiten) — eigen sessie.

Stap 1-3 leveren al een werkende kloonknop voor evenementen; de knop op een activiteitenkaart verbergen tot 127-E klaar is, is één Visibility-conditie op de page parameter type.

Testen

  • Kloon een evenement met en zonder logo/foto's, en één met meerdere categorieën (testnode 222818 heeft er drie).
  • Klonen van een ongepubliceerde aanmelding moet gewoon werken — de kloonbron kijkt niet naar status. Juist dát is het scenario: een afgekeurde of nog wachtende aanmelding opnieuw indienen.
  • Controleer dat de datumvelden leeg zijn en dat indienen zonder datum geweigerd wordt.
  • Na een kloon: terug naar Mijn profiel → Nieuw evenement → formulier moet leeg zijn (127-F).
  • Controleer in de eerste echte respons dat logo_url op https:// staat.

121 · 15 verweesde mappen in lib/ · Eigenaar: Bob (Claude verwijdert niets)

Gevonden 2026-09-20 bij het committen van de achterstallige working tree. Deze mappen staan in de projectmap en in git, maar komen niet meer voor in een verse export — ze zijn ooit in de builder verwijderd of hernoemd, en een export ruimt de lokale map niet op (bekend patroon, staat in CLAUDE.md).

lib/favorieten_copy                                          lib/kanweg_test_upload
lib/favorieten_copy2                                         lib/shared/drawer_component_copy
lib/horecagelegenhedenoverzicht/horecagelegenheid_event_tabel_component_copy
lib/kanweg  (+ kanweg_header_buttons_component_copy,         lib/shared/geen_evenementen_component
     kanweg_home_copy, kanweg_horecagelegenheden_overzicht,  lib/shared/header_current_copy
     kanweghorecagelegenheid_current_copy)                   lib/uitgaanspaginas/home_copy2
                                                             lib/uitgaanspaginas/home_uitgaan_slider_component_copy
                                                             lib/uitgaanspaginas/home_uitgaantabel_kaart_component_copy

Waarom het meer is dan rommel: twee ervan geven een harde compile-error. kanweg/kanweghorecagelegenheid_current_copy en shared/drawer_component_copy roepen KanwegHorecagelegenhedenOverzichtWidget aan, en lib/index.dart exporteert die map niet (meer). dart analyze op de projectmap geeft daardoor 2 errors, terwijl een verse export er 0 geeft.

⚠️ Die twee errors stonden al op master vóór de commit van 2026-09-20 — het is dus geen regressie van die commit, maar ze blokkeren wel een lokale build vanuit de projectmap. Voor de release maakt het niets uit zolang je via FlutterFlow Deploy bouwt (dat vertrekt vanuit de cloud-staat, niet vanuit deze map).

Doen, als je de projectmap weer wilt kunnen builden:

cd /home/bob/Projects/ff-app/uitgaanskrant-1qhvtd
git rm -r --cached -q lib/favorieten_copy lib/favorieten_copy2 lib/kanweg lib/kanweg_test_upload lib/shared/drawer_component_copy lib/shared/geen_evenementen_component lib/shared/header_current_copy lib/uitgaanspaginas/home_copy2 lib/uitgaanspaginas/home_uitgaan_slider_component_copy lib/uitgaanspaginas/home_uitgaantabel_kaart_component_copy lib/horecagelegenhedenoverzicht/horecagelegenheid_event_tabel_component_copy
rm -rf lib/favorieten_copy lib/favorieten_copy2 lib/kanweg lib/kanweg_test_upload lib/shared/drawer_component_copy lib/shared/geen_evenementen_component lib/shared/header_current_copy lib/uitgaanspaginas/home_copy2 lib/uitgaanspaginas/home_uitgaan_slider_component_copy lib/uitgaanspaginas/home_uitgaantabel_kaart_component_copy lib/horecagelegenhedenoverzicht/horecagelegenheid_event_tabel_component_copy
fvm dart analyze lib/ | grep -c "^  error -"   # hoort 0 te zijn

⚠️ Controleer vóór het uitvoeren dat favorieten_copy2 en header_current_copy geen veiligheidskopieën zijn die je nog wilt bewaren — ze zitten sinds commit 98f0b74 in de historie, dus je kunt ze altijd terughalen, maar kijk er even naar.

120 · 12 hardcoded kleuren aan thematokens binden · Eigenaar: Bob (of Claude bij een breder venster)

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 ColorsAlternate resp. Tertiary. Sluit met de X, niet met Escape (Escape laat de tree-selectie terugspringen naar de pagina-root).

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 (09B34ATertiary) 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.

117 · ✅ Kalenderpermissie eruit — AF (2026-09-20)

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.

118 · AdMob: is het EU-consentbericht gepubliceerd? · Eigenaar: Bob

De app-kant is in orde — main.dart:32 roept adMobRequestConsent() aan en flutter_flow/admob_util.dart doet de volledige UMP-flow (requestConsentInfoUpdateisConsentFormAvailableConsentForm.show).

Maar die flow doet niets als er in de AdMob-console geen bericht klaarstaat: isConsentFormAvailable() geeft dan false en de app slaat het formulier stil over. Gevolg in de EU: geen geldige toestemming, dus alleen niet-gepersonaliseerde advertenties (lagere opbrengst), en Google kan de serving beperken.

Doen: AdMob → Privacy & messagingEU regulations (GDPR) → bericht maken voor de app en op Gepubliceerd zetten. Daarna bij de eerste release-test controleren dat het scherm ook echt verschijnt bij een verse installatie.

⚠️ Niet te verwarren met de FlutterFlow-toggle. Bob liet 2026-09-20 zien dat App Settings → AdMob → "Show GDPR Consent Dialog at App Launch" aan staat — dat is de app-kant en die was al goed (die toggle genereert juist de adMobRequestConsent()-aanroep in main.dart). De ontbrekende helft zit op admob.google.com, niet in FlutterFlow. Zonder gepubliceerd bericht vraagt de app netjes om een formulier, krijgt er geen, en gaat stil verder.

🔸 Terzijde uit diezelfde screenshot: Ad content filtering staat op Unset. Dat is AdMob's standaard en prima, maar het loont om er bij G2.5/G2.6 (content rating + doelgroep) bewust een waarde bij te kiezen zodat de advertenties passen bij wat je bij Play aangeeft.

65 · Release-build — wat er NOG getest moet worden · Eigenaar: Bob

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

📌 Livegang-checks die AL goed zijn (gemeten 2026-09-19, niet opnieuw doen)

  • De deel-URL werkt. 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.
  • De support-URL's geven 200: /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).

✅ API-oppervlak dichtgezet — KLAAR (2026-09-20)

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.

📌 Views-resource: al beperkt, niets te doen

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

📌 Livegang-checks die AL goed zijn — aanvulling 2026-09-20 (Claude)

Alle vijf nagemeten op de verse export en op productie; niets te doen, hier genoteerd zodat niemand het opnieuw uitzoekt:

  • 16 KB page size (Play-eis voor nieuwe apps sinds 1 nov 2025): in orde. Gemeten op ~/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.
  • targetSdk: 36 (android/app/build.gradle:62), ruim boven de Play-eis van 35.
  • Het privacybeleid dekt de app-specifieke verwerking. /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.
  • De twee uitgaande gebruikers-URL's geven anoniem 200: /nl/user/register (de knop Account aanmaken op Favorieten) en /nl/user/password. Zelfde val als bij G2.2, maar hier dus goed.
  • De UMP-consentcode zit in de app (main.dart:32) — of het bericht ook echt verschijnt hangt af van de AdMob-console, zie taak 118.

🚦 Bij livegang — schakelaars en opruimklussen · Eigenaar: Bob

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

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

afgewezen alternatief

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.

⚠️ 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', <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 TextFormFields:

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 een geslaagde drupalLogin:

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

  1. Stap 1: Auto Fill Hint-toggle op beide velden weer uit. In een verse export verdwijnt dan autofillHints: uit lib/login/login/login_widget.dart — controleer met grep -c autofillHints lib/login/login/login_widget.dart, hoort 0 te zijn.
  2. Stap 2: de actie loskoppelen van de loginknop (Actions-tab → ⋮ op de actie-node → Delete Action) en daarna de custom action zelf weggooien. Controle: 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.

📚 Referentie — geen taak, wél lezen vóór je iets opnieuw onderzoekt

#

📌 Nagemeten, niet opnieuw onderzoeken

  • Het P1-15-patroon is projectbreed schoon. Alle 16 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.
  • P1-45 is echt weg: 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.
  • Ongelezen component-parameters: alleen EvenementInfo (6 van de 12, nagemeten en juist), de wees hierboven, en HorecagelegenheidoverzichtKaart.nid (restant van het verwijderde hartje).

🚫 Bob's besluiten 2026-09-13 — niet meer voorstellen

  • Titels met onzichtbare tekens opschonen (was taak 42 / P1-47): AFGEWEZEN. Bob: "we wachten tot horecagelegenheden het zelf aanpassen, of we passen het als editors een keer aan. Het is heel veel oude data, daar ga ik geen tijd in steken. Als ik er tegenaan loop, pas ik het aan." Niet opnieuw agenderen. Ter informatie, zodat het effect bekend is: 18 van de eerste 100 zaken in het horeca-overzicht staan hierdoor buiten de alfabetische volgorde, en ze staan allemaal bovenaan (MySQL sorteert op de rauwe node.title, een spatie komt vóór de A). De eerste kaarten die een gebruiker ziet zijn dus willekeurig.
  • minSdkVersion-beslispunt (was taak 43): VERVALLEN, er valt niets in te stellen. Bob's vraag ("dat gaat toch automatisch via FlutterFlow?") is terecht. Gecontroleerd 2026-09-13: 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.
  • P2-7 opruimronde (was taak 44): naar de livegang-lijst.
  • Engelse vertalingen (was taak 45): naar de livegang-lijst, deels al gedaan. Bob 2026-09-13: "dit zullen we nog wel een paar keer krijgen, nieuwe pagina's met vertalingen die missen, en dat wil ik later in 1x."
  • Events zonder categorie (taak 32 deel B): Bob heeft er een eigen taak van gemaakt. Wél een probleem, géén livegang-blokker — zijn expliciete oordeel.

📊 Contentstand productie — hermeten 2026-09-19 (Claude)

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.

📌 Taak 30 · dubbele rijen in flutterflow_events — bekend, geen impact

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

📊 Contentmeting 2026-09-20 (Claude) — het getal DAALT

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:

  • 25 events hebben een leeg/onleesbaar 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.
  • De 17.143 opgehaalde rijen tegen 16.080 unieke nids bevestigt het bekende duplicatenpatroon uit taak 30 (~1.060 overtollige rijen), dat de app niet raakt.

✅ Pre-livegang EXTERNE checks — gemeten 2026-09-20 (Claude, browserloos)

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.

🔍 Pre-livegang code-audit — UITGEVOERD EN AFGEWERKT 2026-09-20 (Claude)

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:

  • Neongroen categorielabel in de horeca-agenda gefixt. De 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.
  • P1-17 is rond voor de app: 0 ontbrekende EN-vertalingen. De zes gaten in 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):

  • 🟠 De lokale repo loopt achter op de builder: 15 mappen onder 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.


🔑 130 · Inloggen met Facebook/Google via OneAll · Eigenaar: Bob (Drupal) + Claude (app)

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:

  1. Embedded WebView mag niet. Google weigert OAuth in een WebView (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.
  2. De app werkt niet met een cookiejar. Hij bewaart 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.

Stappen

# Wie Wat
130-A Bob Upgrade naar Personal Advanced + controleer dat uitgaanskrant.com bij de allowed domains staat
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 Bob Plaatsen + aanzetten + testen. Stap-voor-stap in ~/tmp/130-drupal-instructies.md op aegir-o1, inclusief 4 curl-tests. ⚠️ De callback_uri moet de /nl/-prefix hebben (de site draait op pathPrefix: nl/, de widget op /nl/user doet het ook zo)
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 Claude Knoppen op lib/login/login/login_widget.dart onder het bestaande formulier
130-I Claude Vertalingen (P1-17-patroon: globe per widget)
130-J Bob Echte doorloop met een Facebook- én een Google-account op een profile-build. Claude kan dit niet — geen wachtwoorden
130-K Bob Privacyverklaring + Play Data Safety bijwerken: de app ontvangt dan naam/e-mail/avatar van een derde partij
130-L Claude Bevindingen naar CLAUDE.md

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.