# 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 | |---|---|---| | **FOTOBOEK** | Fotoboek: foto's kiezen uit Drupal + beheren | **Claude — bezig** (sessie 2026-09-22). Drupal-kant **AF** (endpoint `mijn_fotos` index+verwijder, live op devbob én productie, getest). App-kant: **eerste plek werkend** — logo op `uitgaansevenementAanmaken`. API Call `MijnFotos` (Predefined Path `items`, cookie-header), page states `fotoboekOpen`/`fotoboekDoel`, knop *Uit fotoboek* (toggle) naast *Logo kiezen*, en een `GridView` (3 kolommen, `$.thumb`) waarvan een tik `createLogoFid`=`$.fid` + `createLogoUrl`=`$.url` zet en het blok sluit. `dart analyze` 0 errors. **Rest:** omzetten naar herbruikbaar component en uitrollen naar de 3 andere upload-plekken (foto's hier + logo/foto's op `stadsactiviteitAanmaken`), Camera/Galerij-knoppen in hetzelfde blok, 5e tab "Mijn foto's" op `mijnProfiel` voor beheer/verwijderen (custom function `fotoVerwijderBody`), lege staat via `LegeLijstComponent` + vertalingen. Volledig plan + endpoint-spec in `CLAUDE.md` (commit 8c73621) | | **133** | Kopie-knop verkleinen (mijnProfiel 2x + MijnAanmeldingen) | **Claude — bezig** (plan af 2026-09-22, wacht op groen licht voor de browser). Variant A (icoon in de rij) op `mijnProfiel`, variant B (compacte chip) op `MijnAanmeldingen`, tooltip erbij, kleuren meteen goed. Volledig plan + terugvalopties onderaan dit bestand | | **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 t/m F** | Nieuwsbrieven-tab op `mijnProfiel` — **Claude, bezig (2026-09-21)** | **Backend staat live en is getest; klaar om te bouwen** zodra de lopende FlutterFlow-sessie klaar is (Bob meldt dat). Vierde tab + waarschuwing bij 0 gevolgde gemeenten + verwijzing vanaf Favorieten + vertalingen (teksten staan klaar). ⚠️ 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 (+ `get_streamed_response_web.dart`/`_http.dart`, zie 2026-09-21) | 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 | Alle 12 zijn exact gelijk aan een bestaand token, dus nu onzichtbaar — maar ze lopen niet mee bij een themawijziging. Eigen blok hieronder | | ~~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 | | **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 `MijnProfiel` → *Bekijk alles*. Dat volstaat; alleen oppakken als je 'm ook direct vanaf Favorieten wilt kunnen bereiken. --- ## 📰 128 · Nieuwsbrieven aan/uit in de app — **grotendeels AF** (2026-09-21) **Open rest — vier punten, alle vier klein:** 1. **128-C's beperking** — `favorieteGemeenteIds` loopt achter na een verse install, waardoor de waarschuwing ten onrechte kan verschijnen. Twee uitgezochte fixroutes staan bij 128-C. 2. **128-E** — verwijzing vanaf Favorieten tab 2 naar de nieuwsbrief-tab (vraagt een page parameter op `mijnProfiel` voor *Initial Tab Index*). 3. **De `status == 2`-regel** uit 128-F (één conditionele `Text`); komt met `custom_nieuwsbrieven_confirm = FALSE` nu nooit voor. 4. **Drupal, Bob:** de omschrijving van tid 36667 is gelijk aan de naam, dus de app toont die regel twee keer onder elkaar. **Wens Bob 2026-09-21.** Op de website kan een ingelogde gebruiker op `/user//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/D · App: vierde tab "Nieuwsbrieven" — **AF** (2026-09-21) Staat live op `mijnProfiel`, achter *Account*, getest op een toestel met bobcity ingelogd: lijst laadt, aan- en uitzetten geeft `200` en de rij springt meteen om. Opbouw zoals gebouwd: - API Call **`Nieuwsbrieven`** (GET, `Cookie: '${sessionName}=${sessid}'`), Predefined Path `items` = `$[:]` (Is List) → null-veilige `NieuwsbrievenCall.items(...)?.toList() ?? []`. - `Column > ListView` met Backend Query + Generate Dynamic Children. - Per rij een `Row` (padding 16/10/16/10) met `Column > [Text $.naam (Title Small), Text $.omschrijving (Body Small, secondaryText)]` en rechts **twee conditionele `IconButton`s**: `toggle_off` (grijs) → subscribe, `toggle_on` (Primary) → unsubscribe. Conditie = custom function `nieuwsbriefAan(item)`, met/zonder *Apply Opposite Statement*. - Actie: `drupalRequest('POST', '…/nieuwsbrieven/(un)subscribe.json', …, FFAppState().userToken, '{"tid": <$.tid>}')` + Rebuild Page. ⚠️ **De omschrijving van tid 36667 is in Drupal gelijk aan de naam** ("Uitgaanskrant voor bezoekers van de horeca" staat er twee keer onder elkaar). Geen app-bug — op te lossen in de term-beschrijving. *Eigenaar: Bob.* ### 128-C · Waarschuwing bij 0 gevolgde gemeenten · **grotendeels af, één beperking** Staat onder de lijst: een rode regel *"Je volgt nog geen gemeenten, dus de wekelijkse agenda blijft leeg. Tik hier om gemeenten te kiezen."* achter `if (!(FFAppState().favorieteGemeenteIds.isNotEmpty))`, met een `Navigate To` naar `selectprovinciegemeente`. Daaronder staat altijd de oranje regel *"De wekelijkse uitgaansagenda gebruikt de gemeenten die je volgt…"* met dezelfde navigatie. Beide vertaald (NL+EN). ⚠️ **Bekende beperking: `favorieteGemeenteIds` wordt alléén gevuld door `SelectStateDropDownComponent`** (die haalt in zijn On Page Load `favorieten_gemeenten.json` op en schrijft de nids naar App State). Het veld is persisted, dus in normaal gebruik klopt het — maar wie zijn gemeenten op de **website** of op een **ander toestel** heeft gekozen en het gemeente-scherm in deze app nog nooit opende, ziet de waarschuwing ten onrechte. Live gezien op emulator-5554 met bobcity: warning stond er, en verdween zodra het gemeente-scherm één keer geopend was. **Wat de fix zou zijn (uitgezocht, niet gebouwd):** op `mijnProfiel` in de On Page Load hetzelfde lijstje ophalen. Dat kan **niet** met `drupalRequest`: die custom action geeft `dynamic` terug en FlutterFlow biedt zo'n output niet aan als bron voor een getypt veld — getest op zowel een `List` App-State-veld als een `List` page state, in beide gevallen rendert de bronnenlijst leeg. Twee werkende routes: 1. een nieuwe **API Call** `FavorietenGemeenten` (GET + Cookie-header, net als `Nieuwsbrieven`) → Update Page State `favGemeenten` met *Action Outputs → JSON Body → No Further Changes*; conditie dan op die page state; **of** 2. `favorieteGemeenteNids` een **non-nullable** return geven (`List` in plaats van `List?`), waarna hij wél als bron verschijnt voor `Update App State → favorieteGemeenteIds → Set Value`. Dat haalt meteen de `!` weg uit `SelectStateDropDownComponent`. Het page-state-veld **`favGemeenten` (`List`) staat al klaar** op `mijnProfiel` — aangemaakt voor route 1, nu nog ongebruikt. **Wie schrijft dat veld? Precies één plek** (gemeten op een verse export): `SelectStateDropDownComponent` (+ zijn twee dode kopieën) vult `FFAppState().favorieteGemeenteIds` in zijn On Page Load. **Favorieten tab 2 doet dat NIET** — die leest zijn lijst rechtstreeks van de server. Wie dus alleen Favorieten opent en nooit het gemeente-keuzescherm, houdt een leeg veld. Dat maakt 128-E een mooie kans: als de tekst daar tóch komt, kan diezelfde tab meteen ook App State bijwerken. ### 128-E · Verwijzing op Favorieten → de nieuwsbrief-tab · Eigenaar: Claude **Wens Bob 2026-09-21.** Op `Favorieten`, tab 2 (*Favoriete gemeenten*), onder de lijst met gemeenten een tekst met een link naar de nieuwsbrieven-tab op `mijnProfiel`. **Technisch: dit is de enige plek in het project waar naar een SPECIFIEKE tab genavigeerd wordt — dat bestaat hier nog niet.** Alle vier de TabControllers (`mijnProfiel`, `home`, `favorieten`) staan op een hardcoded `initialIndex: 0`, en er is nergens een `animateTo`. Recept: 1. Op `mijnProfiel` een **page parameter** toevoegen (bv. `tabIndex`, Integer, optioneel, default 0). 2. De TabBar-eigenschap **Initial Tab Index** daaraan binden. 3. Op Favorieten een `Navigate To` → `mijnProfiel` met `tabIndex` = de index van de nieuwsbrieven-tab. ⚠️ **Bind aan de PAGE PARAMETER, niet aan een page state die je in On Page Load vult.** De `TabController` wordt in `initState` aangemaakt, dus vóór de On-Page-Load-keten (die draait in een `addPostFrameCallback`). Een page state is op dat moment nog leeg en de tab opent alsnog op 0 — hetzelfde patroon als bij de dropdown-valkuil elders in dit bestand. **Tekst** (Bob 2026-09-21: je-vorm, consistent met de rest van de app): - NL: *"Wil je elke week de uitgaansagenda op basis van je favoriete gemeenten ontvangen? Zet hier de nieuwsbrief aan!"* - EN: *"Want the weekly agenda for your favourite municipalities by email? Turn the newsletter on here!"* ### 128-F · Teksten en vertalingen — **AF** (2026-09-21) Alles wat in de tab staat is NL+EN ingevuld via het globe-icoontje, en de 5 EN-vertalingen die bij de tab-verhuizing gewist waren zijn hersteld: `Wachtwoord wijzigen`/`Account verwijderen`/`Uitloggen` en de twee `Kopie`-knoppen (die kregen bij de hernummering nieuwe sleutels, `oxse6aci` en `wcyy4pao` — de sleutels die hier eerder genoteerd stonden zitten in de dode kopie `mijn_profiel_copy4`). Nog **niet** gebouwd: de regel bij `status == 2` ("Check je mail om je inschrijving te bevestigen"). Die stand komt met `custom_nieuwsbrieven_confirm = FALSE` nooit voor; de resource geeft de status wel al terug, dus het is later één conditionele `Text` erbij. ⚠️ De **omschrijvingen van de nieuwsbrieven komen uit Drupal** en krijgen dus géén globe en geen EN-vertaling. Dat is een bewuste beperking. ### 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. ## 🧪 132 · Bevindingen uit de testronde van 2026-09-22 · Eigenaar: nader te bepalen Volledige doorloop op **emulator-5556 (telefoon)** en **emulator-5554 (tablet)** met een verse release-APK uit een eigen export, zowel uitgelogd als ingelogd als bobcity (Bob logde de telefoon halverwege in). `dart analyze` gaf **0 errors**; er is geen enkele exception of overflow in `logcat` gevallen. Offline-gedrag, netwerkherstel, zoeken, datumfilters, categoriefilter, login-foutafhandeling en de nieuwsbrief-toggles werkten alle correct. Hieronder wat wél mis is, op volgorde van ernst. ### 132-A · `HorecagelegenheidCurrent` scrollt niet — halve zaakpagina onbereikbaar Het bestand bevat **0 `SingleChildScrollView`**; de vier tabs zijn kale `Column`s in een `TabBarView`. Op een telefoon (411 dp) past daar ná de fotostrip, titel en tabbalk nog ongeveer één regel in, en de knop *Alle horecagelegenheden* dekt de rest af. Gemeten op BAUT ZUIDAS (nid 50954): van de Info-tab zijn **telefoonnummer, e-mail en de volledige "over"-tekst** niet te bereiken — de regel *Openingstijden …* wordt middendoor gesneden en er is geen scroll. Op tablet valt het minder op, maar het speelt daar ook. Nuance na extra meting: de **Agenda-tab scrollt wél** (die heeft een eigen lijstwidget), maar ook daar legt de knop zich over de onderste kaarten heen. Het zijn dus twee losse dingen: (1) Info/Links/Bezorgen missen scroll, (2) de knop *Alle horecagelegenheden* overlapt op álle vier de tabs de inhoud in plaats van eronder mee te schuiven. Fix: de Column van elke tab scrollbaar maken (let op het recept in `CLAUDE.md`: eerst elke `Expanded` eruit, anders weigert FlutterFlow het). ### 132-B · Zoekterm lekt van Home naar `PUitgaanPage`, met een leeg zoekveld `FFAppState().zoekterm` is globaal, maar `FilterBalkComponent` maakt zijn `TextEditingController` in `initState` **leeg** aan (`_model.textController ??= TextEditingController()`, geen `..text = …`). Reproductie: op Home zoeken op `jazz` → menu → Gemeente → Uitgaan. De lijst toont dan exact de 3 jazz-treffers van Amsterdam (nagemeten tegen `flutterflowmobiel1?zoek=jazz`), terwijl het zoekveld *Zoek op titel* als placeholder toont. De gebruiker ziet een vrijwel lege pagina zonder oorzaak. #### De keten (nagelopen in een verse export, 2026-09-22) 1. Tik op het vergrootglas → `FFAppState().zoekOpen = !zoekOpen`, de controller wordt gewist, `zoekterm = ''`, dan `herlaadLijsten()`. 2. Het veld verschijnt (`if (FFAppState().zoekOpen)`) en op Home én `PUitgaanPage` verdwijnt tegelijk de slider (`if (!FFAppState().zoekOpen)`). 3. Typen → `EasyDebounce` 2000 ms → `FFAppState().zoekterm = controller.text` → `herlaadLijsten()`. 4. De pagina's geven `zoekterm: FFAppState().zoekterm` door aan hun kaartcomponenten (Home 5×, één per tab; `PUitgaanPage` 1×), en die zetten hem in hun Backend Query als query-parameter `zoek`. Alles hierin is App State — behalve de `TextEditingController`, en díe zit in het **component-model**. Bij een paginawissel wordt dat model vers aangemaakt (`_model.textController ??= TextEditingController()`, zonder `text:`), terwijl `zoekterm` én `zoekOpen` blijven staan. Vandaar: veld open en leeg, filter actief. ⚠️ Tweede symptoom van dezelfde oorzaak: omdat `zoekOpen` ook meegaat, is op de nieuwe pagina óók de **slider verborgen**. Dat maakt het beeld nog verwarrender. `zoekterm` is **niet** persisted, dus een herstart van de app lost het vanzelf op — wat verklaart waarom dit niet eerder opviel. #### Plan van aanpak — één binding, in het gedeelde component Zet op `FilterBalkComponent` → het `TextField` → **Initial Value** een binding naar App State **`zoekterm`**. Dat is alles: het component wordt door beide pagina's gedeeld, dus één wijziging dekt Home en `PUitgaanPage`. Waarom dit de juiste richting is: het ontwerp is al "de filterbalk is globaal" (`zoekOpen` reist immers ook mee). Dan hoort het veld te tónen waarop gefilterd wordt, in plaats van de filter stilletjes te verbergen. De bekende "Initial Value wordt te vroeg gelezen"-valkuil speelt hier **niet**: het veld staat achter `if (FFAppState().zoekOpen)` en bestaat dus pas nadat de gebruiker het zoeken heeft geopend — op dat moment is de App State al gevuld. Builder-stappen: 1. `FilterBalkComponent` openen, het `TextField` in de tree selecteren. 2. Eigenschap **Initial Value** → klik het kleine **⚏-icoontje naast het label**. ⚠️ Níét het icoontje rechts in die rij — dat is de vertaal-globe, die vervangt het hele rechterpaneel door "Setting Translations" (onschadelijk, klik *Back*). 3. Set from Variable → zoekveld: typ **`zoek`** (levert `zoekterm`, `zoekActief` en `zoekOpen` op — bewust een term die meer dan één bron overhoudt, anders rendert de optielijst leeg) → App State → **`zoekterm`**. Rendert de rij leeg: hover er eerst vanaf een andere positie naartoe, in dezelfde tool-aanroep. 4. Confirm. Verwachte export: `_model.textController ??= TextEditingController(text: FFAppState().zoekterm)`. Testen op een toestel: Home → vergrootglas → zoek `jazz` → menu → Gemeente → Uitgaan. Het veld moet nu **`jazz`** tonen bij dezelfde drie treffers. Wis het veld → lijst loopt weer vol (reken op de 2 s debounce). **Alternatief als je liever hebt dat zoeken per scherm is:** zet in plaats daarvan op beide pagina's een On-Page-Load-actie die `zoekterm` én `zoekOpen` reset. Dat is meer werk (twee pagina's) en je verliest de zoekterm bij elke navigatie — maar het is verdedigbaar, want het aanbod verschilt per scherm. Kies er één; de combinatie van beide is zinloos. ### 132-C · Stadsrechten publiceren NIET direct — backend moet mee · Eigenaar: Bob (Drupal) **Bob 2026-09-22: "als iemand stadsrechten heeft, mag de activiteit direct gepubliceerd worden. Alleen gewone gebruikers die een activiteit aanmaken, die moeten eerst in drupal worden goedgekeurd."** De app-tekst klopt dus; de **backend** doet het niet. Huidige stand: `_custom_stadsactiviteit_create()` zet **onvoorwaardelijk `$node->status = 0`** en neemt bewust géén `status`-argument aan, en de docblock noemt `mijn_stadsrechten` letterlijk *"PUUR INFORMATIEF … GEEN toegangscontrole"*. Iedere app-inzending wacht daardoor op de redactie, ook die van bobcity (die stadsrechten heeft op Amsterdam, Drechterland, Enkhuizen en Stede Broec). Wat er moet gebeuren, en waarom het niet triviaal is: - De check hoort **server-side**, op de sessie-gebruiker — nooit een `status` uit de client, anders kan iedereen publiceren. - Vergelijk `field_town_access` van de gebruiker met de **gekozen plaats** van de activiteit. Let op het niveau-probleem uit `CLAUDE.md`: dat veld kan een **provincie-, gemeente- óf plaats-tid** bevatten (bobcity heeft nu gemeenten, eerder stonden er plaatsen en provincies in). Bepaal het niveau met `count(taxonomy_get_parents_all($tid))` → 1 provincie / 2 gemeente / 3 plaats, en kijk of de gekozen plaats **onder** een van de rechten-tids valt. - Valt hij eronder → `status = 1`, anders `status = 0`. - ⚠️ Claude komt niet aan Drupal (staande regel). Claude kan hooguit een script met droogdraai aanleveren; uitvoeren doet Bob. Pas ná deze wijziging klopt de introtekst (`rupw26n4`) met de werkelijkheid. De tekst zelf hoeft dus **niet** aangepast; wel controleren zodra de backend om is. ### 132-D · Vier Engelse labels staan op de verkeerde tekst (`Duplicate`-erfenis) | bestand | sleutel | nl | en (fout) | |---|---|---|---| | `evenement_horecagelegenheid_widget.dart:1253` | `jxlwje5u` | Bezorgen | Opening hours | | idem `:1465` | `usg8k534` | Bezorgt in | Opening hours | | idem `:1549` | `paqq93tp` | Betaalmogelijkheden | Opening hours | | `horecagelegenheid_current_widget.dart:529` | `jwojezxv` | Agenda | User | Verder zijn álle 219 gerenderde sleutels vertaald (0 lege `en`). ### 132-E · Rechts uitgelijnde elementen missen hun rechterpadding Op `EventCurrent` raakt de **agenda-knop** exact de schermrand (de deelknop ernaast heeft wél ruimte), net als het categorielabel eronder. Op tablet valt het **favorietenhartje** op `HorecagelegenheidCurrent` zelfs half buiten beeld. ### 132-F · Omschrijving wordt middendoor gesneden bij meerdere categorielabels Op een Home-kaart met 4 labels (die op 2 regels vallen) wordt de omschrijving zonder "…" afgekapt — zie *Ondernemend Dijk en Waard* op de tab Cultuur & Info. De labels eten de ruimte op die de responsive `Max Lines` veronderstelt. Bij één label klopt het wel. Overweeg de labels op één regel te begrenzen, of de omschrijving één regel korter. ### 132-G · Lege lijst toont een logo i.p.v. een melding · Eigenaar: **Claude — bezig** (onderzoek 2026-09-22 af) Kies je een categorie zonder treffers, dan verschijnt een kaal `logo800px.png`. `LegeLijstComponent` (Icon `event_busy` + `Text`) doet dit al goed op `mijnProfiel`. **Volledige inventaris — 18 plekken in LEVENDE code, 9 bestanden.** De dode kopieën (`kanweg/*`, `home_copy`, `home_copy2`, `p_uitgaan_page_copy{,2,3}`, `drawer_component_copy`, `mijn_profiel_copy*`, `evenement/event`) bevatten nog ~15 logo's; geverifieerd wees (alleen zelfreferenties), overslaan. | Waar | Plekken | Dekt | |---|---|---| | `HomeUitgaantabelKaartComponent` | 1 | **alle 5 Home-tabs** (één component) | | `HomeUitgaanSliderComponent` | 1 | Home-slider (Carousel) | | `PUitgaantabelKaartComponent` | 1 | PUitgaanPage-lijst | | `PUitgaanSliderComponent` | 1 | PUitgaan-slider (Carousel) | | `MijnAanmeldingen` | 1 | staat op `uitgaanskrant.com-applogo.jpg` | | `horecagelegenhedenOverzichtCurrent` | 6 | één per tab | | `HorecagelegenhedenOverzichtProvinciePage` | 6 | ⚠️ **wél bereikbaar**, `drawer_component_widget.dart:1209` | | `thuisBezorgen` | 1 | | | `HorecagelegenheidCurrent:248` | 1 | fotocarousel van een zaak — ander geval, beeld-placeholder verdedigbaar | Daarnaast: **Favorieten tab 1-3 hebben helemáál geen Empty List Widget** (zie 132-H), en de agenda-tab van een zaak gebruikt een eigen component `GeenEvenementenComponentmoetweg` (Icon + vaste tekst) — functioneel al goed, opruimkandidaat. **Carousels:** op #2 en #4 staat de toggle al aan (er staat immers een Image), dus alleen *Widget Type* → Component. Reken er toch op dat dit er twee voor Bob zijn; `CLAUDE.md` meldt die checkbox als voor Claude onbereikbaar. #### i18n — onderzocht 2026-09-22, dit is de kern De `tekst`-parameter is een **letterlijke string**, dus geen globe en geen Engelse vertaling. Drie dingen uitgezocht: 1. **`FFLocalizations.getVariableText({nlText, enText})` bestaat al** in `internationalization.dart` — dat is wat FlutterFlow genereert voor een niet-statische tekst. Nergens in gebruik. 2. ⚠️ **De Global Property `locale` is in dit project ONBRUIKBAAR — altijd `null`.** `MyApp._locale` begint op `null` en `setAppLanguage()` heeft **nul aanroepers** (er is geen taalkiezer). De werkelijke taal komt uit de Localizations-delegate (systeemtaal), en die is in de builder geen bron. Een Visibility-conditie op "huidige taal" werkt hier dus niet. Niet proberen. 3. Een custom **function** kan er niet bij (geen `BuildContext`); een custom **widget** wel. **Drie werkbare routes:** - **D (aanbevolen) — losse componenten per melding.** `Duplicate Component` van `LegeLijstComponent`, `tekst`-parameter eraf, vaste `Text` + globe. Nul condities, per aanroepplek alleen "Empty List Widget → Component → kies". Teksten belanden in `internationalization.dart`, dus de vertaalaudit ziet ze. Kosten: ~10-12 componenten in de lijst. - **C — één component met een `soort`-parameter** (String) en N conditionele `Text`-widgets, elk met eigen i18n-sleutel. Houdt de componentenlijst schoon, maar kost 12× een Visibility-conditie met een letterlijke Second Value (het bekende verstopte veld). Het patroon zelf is bewezen: `mijn_aanmeldingen_widget.dart:408` heeft al `if (widget!.type == 'go_out_event')`. - **A — custom widget** met `getVariableText(nlText:, enText:)` en twee String-parameters. Minste fragiele klikwerk, maar de teksten staan dan niet in `internationalization.dart` (geen globe, geen audit). **Openstaand besluit voor Bob: D, C of A.** Reken op ~10-12 unieke teksten en 22 aanroepplekken (18 hierboven + de 4 bestaande op `mijnProfiel` die van `tekst:` af moeten bij route D of C). ### 132-H · Favorieten is uitgelogd een leeg scherm Tab 1-3 tonen niets en geen uitleg; de uitnodiging *"Met een account bewaar je favoriete gemeenten…"* staat op tab 4, twee tabs naar rechts. Verwijs vanaf de eerste tab naar die knop (of toon `LegeLijstComponent` met die tekst). ### 132-I · Kleine punten - **Build-stempel zichtbaar voor eindgebruikers.** `functions.buildStempel()` staat onderaan de **drawer** én op **Favorieten** en toont `onbekend` zolang er geen `--dart-define=BUILD_TS=…` meegaat. Weghalen vóór livegang, of vullen. - **Datums in `MijnProfiel`** staan ruw (`2026-09-22 06:06:00`) terwijl de rest van de app *dinsdag 22 sep, 14:00* gebruikt. Geldt voor *Mijn evenementen* en *Eigen activiteiten*. - **Persoonlijke agenda toont overal "Binnenkort meer…"**: `favorieten_widget.dart:435` geeft hardcoded `inhoud: null` mee. **Nagemeten 2026-09-22 (respons uit logcat van de ingelogde app): `favorieten_agenda.json` heeft géén omschrijvingsveld.** Volledige veldenset: `node_type, nid, titel, datum, datum_raw, plaats, categorie, logo, adres, postcode, woonplaats, horecagelegenheid, horecagelegenheidNid, matched_via`. De binding is dus niet "vergeten" — er valt niets te binden. Keuze voor Bob: óf een omschrijvingsveld aan de resource toevoegen (Drupal), óf de placeholderregel weghalen zodat de kaart gewoon zonder tekst staat. *(Terzijde: `horecagelegenheidNid` mét hoofdletter N is voor dít endpoint correct — getest, het horeca-blok op de detailpagina komt gewoon door. Niet "gelijktrekken" met de `$.horecagelegenheidnid` van de views.)* - **Zoekveld krijgt geen focus** als je het opent met het vergrootglas — je moet er zelf nog in tikken. - **Login-melding** zegt *"Controleer je gebruikersnaam en wachtwoord"* terwijl het veld *E-mailadres* heet. - **Tabnamen inconsistent**: *Favoriete gemeenten* (kleine g) naast *Favoriete Gelegenheden* (hoofdletter G). - **Testadvertenties staan nog aan** (zichtbaar op `PUitgaanPage` en het horeca-overzicht); op het horeca-overzicht legt de 468×60-banner zich over de onderste kaart heen. Zie ook G0.6. ### 132-J · Waarschuwing "Je volgt nog geen gemeenten" verschijnt ONTERECHT Raakt taak 128. Op `mijnProfiel` → Nieuwsbrieven stond bij bobcity de rode regel *"Je volgt nog geen gemeenten, dus de wekelijkse agenda blijft leeg"* — terwijl die drie favoriete gemeenten heeft (Alblasserdam, Amsterdam, Arnhem, zichtbaar op Favorieten tab 2). De conditie zelf is goed (`if (!(FFAppState().favorieteGemeenteIds.isNotEmpty))`, regel 2302). Het probleem is de **bron**: `favorieteGemeenteIds` wordt op precies één plek gevuld, in de On-Page-Load van `SelectStateDropDownComponent` (`select_state_drop_down_component_widget.dart:115`, via `favorieten_gemeenten.json`). Dat component staat alleen op **Zoek stad**. Wie na het inloggen direct naar Mijn Profiel gaat, heeft dus een lege lijst en krijgt de verkeerde waarschuwing. **Reproductie en bewijs (2026-09-22, telefoon-emulator, ingelogd):** Mijn Profiel → Nieuwsbrieven → rode regel staat er. Daarna menu → Zoek stad → terug naar Nieuwsbrieven → regel is weg. Twee keer zo gelopen. Fix: `mijnProfiel` zelf de lijst laten ophalen in On Page Load en de conditie op een **page state** hangen — exact het patroon dat elders op diezelfde pagina al gebruikt wordt (page state type JSON + *Is List*, gevuld uit een Backend Call, conditie *Is Set and Not Empty*; recept staat in `CLAUDE.md`). Een App State die maar op één scherm gevuld wordt, is hier de verkeerde bron. Let op: de tweede, oranje regel eronder (*"De wekelijkse uitgaansagenda gebruikt de gemeenten die je volgt"*) staat er altijd en is de algemene toelichting. Met 0 gemeenten staan er dus twee bijna gelijke regels onder elkaar — overweeg er één van te maken. ### 132-L · Favoriete gemeente kiezen springt naar Home i.p.v. naar die gemeente · Eigenaar: **Claude — bezig** (onderzoek 2026-09-22 af) **Drie plekken, en één ervan doet het expliciet fout:** | # | Bestand | Nu | |---|---|---| | 1 | `favorieten_widget.dart:552` | `context.pushNamed(HomeWidget.routeName)` — **navigeert letterlijk naar Home** | | 2 | `select_state_drop_down_component_widget.dart:559` | tik op favoriete gemeente → `context.safePop()` | | 3 | `select_state_drop_down_component_widget.dart:458` | knop *Toepassen* → `context.safePop()` | Bij #2 en #3 kom je terug op de pagina waar je vandaan kwam — meestal Home, want de keuzepagina wordt geopend vanaf het headerlogo/de drawer. Kwam je van `PUitgaanPage`, dan sta je daar mét de **oude** `plaats`-queryparameter: de gemeente lijkt gewijzigd (de header leest `gemeenteSelectNaam` uit App State) maar de lijst niet. **Bestemming: `PUitgaanPage` met `plaats = FFAppState().gemeenteSelectId` en `services = 'services_3'`** (= *Uitgaan*, het eerste item onder "Gemeente" in de drawer; die navigeert al precies zo). ✅ **Gemeten 2026-09-22: een gemeente-tid werkt gewoon als `townid`** — dat was het enige echte risico. Amsterdam gemeente `28694` geeft op `services_3` 50 items en op `services_4` 5, tegen plaats `28695` respectievelijk 50 en 2. De gemeente omvat meer plaatsen, dus dit is zelfs ruimer. Geen Drupal-werk nodig. **Aanpak per plek:** twee acties achter elkaar — eerst `safePop` (keuzepagina sluiten), dan `Navigate To PUitgaanPage`. Backstack wordt dan Home → PUitgaanPage, dus de terugknop gedraagt zich normaal. Bij #1 vervangt de Navigate gewoon de bestaande `pushNamed(Home)`. ⚠️ **Bijvangst-bug op dezelfde regels:** `favorieten_widget.dart:548` vult `provincieSelectName` met `$.parent_nid` in plaats van `$.parent_titel` — dus de provincienaam is er een getal. In `select_state_drop_down_component_widget.dart:556` staat het wél goed. Meenemen bij deze taak. ### 132-M · Stadseditor-overzicht toont geen ongepubliceerde activiteiten · Eigenaar: nader te bepalen **Onderzocht 2026-09-22 — het is NIET de status, het is het `komend=1`-filter.** De lijst *Eigen activiteiten* (tab Stadseditor op `mijnProfiel`, regel 1180) roept `MijnAanmeldingenCall` aan met `type: 'activity'`, `komend: '1'`, `limit: 5`. Wat er goed is: - Het endpoint `_custom_mijn_aanmeldingen()` filtert **niet** op status — er staat zelfs expliciet in het commentaar dat status-0 juist mee moet, en het levert `status` + `status_label` ("Wacht op goedkeuring") uit. - De app filtert ook niet: `isOngepubliceerd()` wordt alleen gebruikt voor een snackbar bij aantikken (`mijn_profiel_widget.dart:1285`), niet voor zichtbaarheid. Wat het wél is: bij `komend=1` doet het endpoint een INNER JOIN op `field_data_field_date` met `field_date_value >= vandaag`. Gemeten voor **bobcity (uid 3621)**: | nid | status | datum | komt door het filter? | |---|---|---|---| | 222823 | 0 | 2026-09-26 | ✅ (aangemaakt vandaag 15:03 UTC) | | 222818 | 1 | 2026-09-25 | ✅ | | 222819 | 0 | 2026-09-20 | ❌ verleden | | 214445 | 0 | 2026-09-03 | ❌ verleden | Dus 2 van de 3 ongepubliceerde vallen weg op **datum**, niet op status — en de enige die er wel doorkomt bestond vanochtend nog niet. Vandaar de indruk "alleen gepubliceerde". **Dat het juist ongepubliceerde treft is geen toeval:** die liggen bij de redactie, dus hun datum kan verstrijken terwijl ze wachten — en dan verdwijnen ze stil uit het overzicht van de indiener. Dat is de echte bug. **Twee oplossingen:** - **App-kant (1 wijziging, geen Drupal):** laat `komend` leeg op déze lijst. Het endpoint sorteert dan op `created DESC` — voor een "eigen activiteiten"-lijstje eigenlijk logischer. Nadeel: verlopen gepubliceerde komen ook mee. - **Server-kant (aanbevolen, Eigenaar: Bob):** laat `komend=1` betekenen "toekomstig **óf** nog niet gepubliceerd", in de `if ($komend)`-tak van `custom.mijn_aanmeldingen.inc`: ```php $q->condition(db_or() ->condition('d.field_date_value', gmdate('Y-m-d'), '>=') ->condition('n.status', 0)); ``` Raakt ook *Mijn evenementen* (`type=go_out_event`), wat daar hetzelfde gewenste effect heeft. ### 132-K · `&` in de categorie · ✅ GEFIXT OP DEVBOB, productie nog te doen **Stand 2026-09-22: Bob heeft beide regels handmatig aangepast op devbob; Claude heeft het daar geverifieerd. Alleen de uitrol naar productie rest.** Geverifieerd op devbob (alles read-only gemeten): - beide regels staan goed, `php -l` schoon op allebei; - `diff` tegen productie toont **alleen** de bedoelde wijziging — plus twee regels commentaar, één weggehaalde trailing space en één lege regel aan het eind. Niets anders is bij het plakken meegekomen; - de logica: `'Kunst & Cultuur, Theater'` → `["Kunst & Cultuur","Theater"]`, en de dubbel geëscapete `'R&amp;B'` → `["R&B"]`. De komma-split blijft dus intact en de herhaalde decode-lus werkt; - **`favorieten_horeca` live gedraaid** voor uid 3621 (bobcity): *Luxor Live* → `["Kunst & Cultuur","Theater"]` — precies het geval dat op productie nog fout gaat. Accenten en slashes blijven heel (`Muziekcafé/Poppodium`, `Theatercafé`), en een zaak met 7 categorieën blijft netjes gesplitst; - **`mijn_horecagelegenheden` live gedraaid** voor uid 2 en 603: ook daar komen `Kunst & Cultuur` en `Feesten & partijen` schoon door. Uitrollen naar productie: dezelfde twee regels in `/data/disk/o1/static/uitgaanskrant/sites/all/modules/custom/`, daarna `drush @uitgaanskrant.com cc all`. Geen Cloudflare-purge nodig — beide endpoints zijn sessie-gebonden en matchen geen Cache Rule. Opruimen: `/data/disk/o1/tmp/fix-132k.py` staat er nog (een script dat Claude had klaargezet en dat Bob niet wilde gebruiken; het heeft niets gewijzigd, alleen een droogdraai gedaan). Mag weg met `rm`. #### Oorspronkelijke analyse Favorieten tab 3 toont bij Luxor Live het label **`Kunst & Cultuur`**. Nagemeten in de rauwe respons: `"categorie":["Kunst & Cultuur","Theater"]`. Dit is het bekende `_custom_clean_html()`-patroon uit `CLAUDE.md`: het veld `categorie` staat voor dit endpoint niet in de `$fields_to_clean`-lijst. De views leveren diezelfde categorie wél schoon uit ("Kunst & Cultuur"), dus het is endpoint-specifiek. **Vindplaats opgezocht op de server (2026-09-22, alleen gelezen).** Het zit op **twee** plekken, met identieke code — beide bouwen hun resultaat zélf op uit `views_get_view('flutterflowmobiel_establishments')` en draaien dus niet langs de opschoning in `custom_services_request_postprocess_alter()`, ondanks wat de docblock erboven suggereert: | bestand | regel | resource | |---|---|---| | `custom.favorites_flag.inc` | **316** | `favorieten_horeca` (Favorieten tab 3) | | `custom.evenementen_aanmaken.inc` | **306** | `mijn_horecagelegenheden` (Mijn Profiel) | In beide gevallen krijgen `titel`, `adres` en `plaats` netjes `_custom_clean_html()` mee en **`categorie` als enige niet**: ```php 'plaats' => _custom_clean_html($plaats), 'logo' => str_replace('devbob.', '', $logo), 'categorie' => _custom_parse_komma_to_array($categorie_raw), // <- hier ``` Fix, één regel per bestand: ```php 'categorie' => _custom_clean_html(_custom_parse_komma_to_array($categorie_raw)), ``` **Decodeer ná het splitsen, niet ervóór.** `_custom_clean_html()` handelt arrays recursief af (`custom.module:1860`), dus dit werkt direct op de lijst. Zou je `$categorie_raw` eerst decoderen, dan kan een `,` een extra komma opleveren en daarmee de split op `', '` stukmaken. Het tweede geval (`mijn_horecagelegenheden`) is niet visueel bevestigd — daarvoor moet je eigen zaak toevallig een `&` in een categorie hebben — maar de code is regel voor regel dezelfde, dus fix ze samen. De volledige path op de server: `/data/disk/o1/static/uitgaanskrant/sites/all/modules/custom/`. Controleer in dezelfde moeite de andere favorieten-resources (`favorieten_agenda.json`, `favorieten_gemeenten.json`) — in de opgevangen agenda-respons zaten geen entiteiten, maar daar zat toevallig ook geen `&` in. ### Wat expliciet GOED was Offline starten (rode banner *Geen verbinding met uitgaanskrant.com* + werkende knop **Opnieuw**, geen crash, geen grijs blok) · netwerkherstel · de vier datumfilters (nagemeten tegen de API) · zoeken op titel · de tab-bewuste categoriedropdown · login-foutafhandeling · de nieuwsbrief-toggles, die per rij onafhankelijk schakelen en netjes `subscribe`/`unsubscribe` met CSRF-token sturen (status 200) · `EventCurrent` inclusief het horeca-blok, ook als je er via de persoonlijke agenda komt · de responsive 1/2/3-kolommen op tablet. ## 🔔 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: `/`. **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. ## 🔁 133 · Kopie-knop verkleinen (3 plekken) · Eigenaar: **Claude — bezig** (plan af 2026-09-22, wacht op groen licht voor de browser) Besluit Bob 2026-09-22: de `Kopie`-knop onder de kaarten is te groot. **Variant A** (icoon ín de rij) op `mijnProfiel`, **variant B** (compacte chip) op `MijnAanmeldingen`, met tooltip, en de kleuren meteen goed. **Waarom hij zo groot oogt:** alle drie de knoppen hebben `color: secondaryBackground` op een kaart die zelf óók `secondaryBackground` is — een onzichtbaar vlak van 40 dp met alleen het woord "Kopie" erin. ### Waar het zit (verse export 2026-09-22) | # | Bestand | Structuur nu | i18n | |---|---|---|---| | 1 | `mijn_profiel_widget.dart:617` (evenementen) | `Column [ ListTile(+chevron), Align(1,0) > Button ]` | `oxse6aci` | | 2 | `mijn_profiel_widget.dart:1430` (stadsactiviteiten) | idem | `wcyy4pao` | | 3 | `mijn_aanmeldingen_widget.dart:386` | `Column [ Padding>Column(logo/titel/datum/status), Button ]`, knop **gecentreerd** (geen `width`) | `g4pbs1et` | Knop-opties op alle drie identiek: `height 40`, `padding 16/0/16/0`, `color secondaryBackground`, `titleSmall` + `primaryText`, `elevation 0`, `borderRadius 8`. ### Doelstijl - **A (plek 1 en 2):** `FlutterFlowIconButton`, `content_copy`, `buttonSize 34`, `icon size 20`, `borderRadius 8`, **Fill Color `primaryBackground`**, **Icon Color `secondary`** (#FF680D — merk-DNA: oranje = actie). Rechts ín de rij, met **8 dp rechtermarge** (zie 132-E). De chevron (`arrow_forward_ios_rounded`) vervalt: puur decoratief, de hele rij is al tikbaar. - **B (plek 3):** de `Button` blijft, maar `height 40 → 28`, `padding 16 → 10`, `borderRadius 8 → 14` (pil), `titleSmall → bodySmall`, **tekstkleur `secondary`**, **`borderSide` 1 px `secondary`**, Fill Color op **transparant**, en een **icoon `content_copy`** vóór de tekst (`iconPadding` links 0, rechts 4). Daarna `Align(1,0)` eromheen + 12 dp rechterpadding. - **Tooltip** op alle drie (Wrap Widget → 4e rij, 1e icoon), `triggerMode` **longPress** (niet tap — de knop heeft zelf al een onTap, anders dan het favorietenhartje op `HorecagelegenheidCurrent`). Referentiepatroon: `horecagelegenheid_current_widget.dart:336-437`. ### Teksten (nieuwe i18n-sleutels via de globe) | plek | NL | EN | |---|---|---| | 1 en 3 | Kopieer naar een nieuw evenement | Copy to a new event | | 2 | Kopieer naar een nieuwe activiteit | Copy to a new activity | Bij A verdwijnt de knoptekst; `oxse6aci` en `wcyy4pao` blijven dan leeg staan (niet erg — een lege `en` valt niet terug op NL, maar er is ook geen NL meer). Bij B blijft `g4pbs1et` = Kopie/Copy gewoon staan. ### Stappen — plek 1 en 2 (variant A) 1. Tree-filter `ListTile` → de juiste node (1e = evenementen, 2e = stadsactiviteiten). 2. `ListTile` → Wrap Widget → **Container**, breedte op **∞** (het ∞-knopje in het Width-veld; let op: dat is een toggle, één klik). 3. Die `Container` → Wrap Widget → **Stack**. 4. De bestaande `Align > Button` de `Stack` in slepen (drop op de `Stack`-rij = positie 0), daarna de `Container`-rij óók op de `Stack`-rij droppen zodat de volgorde `[Container, Align]` wordt — anders ligt de knop áchter de rij. Eén sleep per tool-aanroep, verse `zoom` ertussen. 5. `Align` op **(1.0, 0.0)** + rechterpadding 8. 6. De `Button` → rechtsklik → **Replace** → `IconButton`. **Dit is de kritieke stap**: Replace behoudt de actieketen. Zie de terugval hieronder als `IconButton` niet in de Replace-lijst staat. 7. Icoon + kleuren + maten uit "Doelstijl". 8. `ListTile` → **Trailing Icon** weghalen (chevron) en `contentPadding` rechts **12 → 52**, zodat een lange titel niet onder de knop loopt. 9. Wrap de `IconButton` in een **Tooltip**, tekst + EN via de globe. ⚠️ **Twee risico's, met terugval:** - **Een `ListTile` in een `Stack`** is in dit project nergens precedent, en FlutterFlow weigert een `ListTile` in een `Row` ("Invalid Action") omdat die geen intrinsieke breedte heeft. Stap 2 (de Container met breedte ∞) is er juist om dat te voorkomen. Weigert stap 3 alsnog: **val voor plek 1 en 2 terug op variant B** — dan is de hele taak één set knop-opties + tooltip, zonder structuurwijziging. - **Replace → IconButton** is niet bevestigd (bij een `Text` biedt de dialoog alleen RichText/TextField/Button). Lukt het niet: **NIET** Copy/Paste Action Chain gebruiken — de keten hangt volledig aan het loop-item (`$.nid`, `$.horecagelegenheidnid`) en die bindingen sneuvelen bij plakken. Terugval dan: de bestaande `Button` houden en hem icoon-only maken (knoptekst leeghalen via de globe + `content_copy` als icoon + `height/width 34`, `padding 0`). Zelfde resultaat, nul risico op verloren bindingen. ### Stappen — plek 3 (variant B) 1. `Button` selecteren → knop-opties uit "Doelstijl" zetten. 2. Icoon toevoegen (Add Icon → `content_copy`). 3. Wrap Widget → **Align** (1.0, 0.0) + rechterpadding 12. 4. Wrap Widget → **Tooltip**, tekst + EN via de globe. ### Verificatie (verse export, nulmeting van 2026-09-22 erbij) | check | nu | na | |---|---|---| | `grep -c "FlutterFlowIconButton(" lib/mijn_profiel/mijn_profiel_widget.dart` | 2 | 4 | | `grep -c "arrow_forward_ios_rounded" lib/mijn_profiel/mijn_profiel_widget.dart` | 5 | 3 | | `grep -c "AlignedTooltip(" lib/mijn_profiel/mijn_profiel_widget.dart` | 0 | 2 | | `grep -c "Stack(" lib/mijn_profiel/mijn_profiel_widget.dart` | 0 | 2 | | `grep -c "AlignedTooltip(" lib/mijn_aanmeldingen/mijn_aanmeldingen_widget.dart` | 0 | 1 | | `grep -c "FFButtonWidget(" lib/mijn_aanmeldingen/mijn_aanmeldingen_widget.dart` | 1 | 1 | Plus: `height: 28.0` en `borderSide` in de opties van plek 3, en per plek dat de actieketen intact is — tel de navigatieparameters (`grep -c "serializeParam" ...` rond elke knop) en controleer dat `$.nid` en `$.horecagelegenheidnid` nog aan het **loop-item** hangen, niet aan de respons. ## 🅖 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) - [x] **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. - [x] **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. - [x] **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". - [x] **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). - [x] **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. - [x] **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.app` — **onveranderlijk 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 ads* → **JA** (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 **E**ntree" (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 @ 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` / `ketenPlaats` → `split('|')[n]`). Die ene parameter is bewust: een lookup op de hele respons vraagt een custom function met meerdere argumenten, en die binding loopt in de Action Flow Editor structureel vast (zie `CLAUDE.md`). On Selected → `Update Page State` voor `createProvincieID`/`createGemeenteID`/`createPlaatsID` + `Set Form Field` op de drie dropdowns. Een lege `Set Form Field`-waarde wordt geweigerd ("Value cannot be empty"), dus voor de lege delen van de keten is **Reset Form Fields** nodig — wat meteen de tweede helft van taak 126 afhandelt. **Voorvullen, niet verbergen** (besluit Bob 2026-09-20): met verbergen kun je achteraf niet meer wisselen zonder eerst de rechten-dropdown leeg te maken. ### 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. ### 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/backend/api_requests/get_streamed_response_web.dart (+ _http.dart) 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:** ze vervuilen `dart analyze`. Stand 2026-09-22 op een verse export in de projectmap: **7 errors, alle 7 uit deze weesbestanden** — 5× `get_streamed_response_web.dart` (importeert `fetch_client`, dat niet in `pubspec.yaml` staat) en 2× `KanwegHorecagelegenhedenOverzichtWidget` in `kanweg/kanweghorecagelegenheid_current_copy` en `shared/drawer_component_copy`, omdat `lib/index.dart` die map niet meer exporteert. ⚠️ **Nagemeten: de BUILD breekt hier niet van** — niets in de levende code importeert deze bestanden, dus de compiler komt er nooit langs (eerder stond hier "harde compile-error"; dat klopt niet). Het kost je wel je nulmeting: zolang deze mappen er staan is "0 errors" niet meer bruikbaar als controle na een export. **Er is er 2026-09-21 een derde bijgekomen, met een nieuwe oorzaak.** De export levert sindsdien alleen nog `get_streamed_response.dart` (die kaal `http` gebruikt); de oude `_web.dart`/`_http.dart` uit de conditional-import-opzet komen niet meer mee. Tegelijk haalde FlutterFlow **`fetch_api` en `fetch_client` uit `pubspec.yaml`** — terecht, want niemand gebruikt ze nog. Gevolg: `get_streamed_response_web.dart` staat er nog, importeert een package dat er niet meer is, en levert **5 analysefouten**. Niets importeert dat bestand (`grep` bevestigd), dus de **build** breekt er niet op — alleen `dart analyze`. Samen met de twee hierboven: `dart analyze` op de projectmap geeft **7 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:** ```bash 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 lib/backend/api_requests/get_streamed_response_web.dart lib/backend/api_requests/get_streamed_response_http.dart 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 lib/backend/api_requests/get_streamed_response_web.dart lib/backend/api_requests/get_streamed_response_http.dart 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 Colors** → *Alternate* 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 (`09B34A` → **Tertiary**) zijn wél Fill Colors: `HorecagelegenheidoverzichtKaart` en `tagCategorieComponent`, allebei het categorielabel. **De 12e kleur slaan we over:** `Color(0xFFFACFC6)` in een gradient op `SliderUitgaanComponentSmallCurrent`. Dat is roze, hoort bij géén token, en dat component heeft **nul gebruikers** — het staat al op de opruimlijst. ⚠️ **Claude kwam hier 2026-09-20 niet doorheen.** Navigeren, panelen wisselen en de linkerrail werkten normaal, maar **geen enkele klik op een widget-tree-rij selecteerde iets** — ook de root niet, in ~10 pogingen op wisselende x/y. Een klik op de chevron ernaast deed wél iets (deselecteerde de root), dus de events komen aan; alleen de rij-selectie niet. Zonder selectie verschijnt het eigenschappen-paneel niet en is er geen kleur te zetten. Bob's venster was toen **1266x952**; een breder venster is het eerste om te proberen. Geen haast, geen blokker — prima klusje voor tussendoor. ### ~~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 (`requestConsentInfoUpdate` → `isConsentFormAvailable` → `ConsentForm.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 & messaging** → *EU 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/` (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/.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 `StaggeredView`s draaien op `filterHorecagelegenheden(...)`, `.take(25)` is weg (6x `.take(1000)`), de routes wijzen naar `...Current`, en de twee lege `TextField`-placeholders zijn opgeruimd. **F is geschrapt** op Bob's verzoek. **B — categoriedropdown per tab: ✅ af** (nagemeten 2026-09-19 avond): de `FlutterFlowDropDown` op `horecagelegenhedenOverzichtCurrent` hangt aan `horcatOptiesVoorTab` / `horcatLabelsVoorTab` met de TabBar-index. Daarmee is P2-24 helemaal rond; dit blok kan weg zodra Bob 'm op het toestel goedkeurt. **Niet oplosbaar, geaccepteerd:** FlutterFlow zet op de `On Change`-trigger van het zoekveld een **`EasyDebounce` van 2000 ms** die nergens instelbaar is. De lijst ververst dus ~2 s nadat je stopt met typen. ### 🔴 Taak 28 / 109 · 618 toekomstige events zonder categorie komen NERGENS in de app · Eigenaar: Bob **Nagemeten door Claude op productie, 2026-09-19 17:44** (alle zeven displays volledig gepagineerd, nids naast `flutterflow_events` gelegd, geclassificeerd op **datum + tijd** tegen de `>= -2 uur`-grens van de master): | | | |---|---| | echte toekomstige events | **6.684** | | bereiken geen enkele tab | **618 (9 %)** | | ... daarvan zonder categorie | **618 — dus 100 %** | | ... daarvan mét categorie | **0** | **Twee dingen die dit uitwijst.** (1) De zeven displays dekken **alle** categorieën correct af — er is geen categorie meer die nergens landt, dus dát deel van taak 28 is klaar. (2) Het hele resterende lek is één oorzaak: een event **zonder** categorie valt door elk display-filter heen en is in de app dus onvindbaar. Stand 12 sep: 597. Nu: 618. Het loopt dus licht op — de fix van 19 sep werkt kennelijk niet (of alleen voorwaarts, terwijl de bestaande nodes blijven staan). Voorbeelden (allemaal ruim in de toekomst, dus geen datumkwestie): `222806` Steve 'n' Seagulls (20 dec, Weert) · `222737` Altruism Label Night (21 nov, Maastricht) · `222703` BRECHTJE Maresa (1 okt, Groningen). **✅ Besluit Bob 2026-09-19: dit is een IMPORT-probleem en wordt bij de importer afgevangen.** Geen Drupal-hook of extra display nodig; niet opnieuw agenderen. Wel bij een volgende contentmeting nakijken of het getal daalt (nu 618).
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 `` door je eigen uid: ``` ssh aegir-o1 "drush @uitgaanskrant.com php-eval \"db_delete('sessions')->condition('uid', )->execute();\"" ``` 3. Ververs Favorieten in de app. Verwacht: rode balk *"Je sessie is verlopen. Log opnieuw in."* met knop **Inloggen**, en de pagina klapt naar zijn uitgelogde weergave. Werkt het niet, dan zit het in laag 2 (`VerbindingsBanner`), niet in de detectie — die is hierboven al bewezen. Controleer dan `adb logcat | grep "SESSIE VERLOPEN"`: staat die regel er wél, dan komt de melding niet door en is het de SnackBar-logica. ### Taak 115 · Autofill: wachtwoordmanager op de loginpagina · Eigenaar: Claude (Bob plant een losse sessie) **Doel:** de gebruiker zijn inloggegevens door Android/iOS laten opslaan en invullen, zodat opnieuw inloggen twee tikken kost. De app slaat bewust géén wachtwoord op — dat blijft bij het besturingssysteem. **Context:** sessies zijn sinds 2026-09-20 een jaar geldig (zie `CLAUDE.md`), dus dit is comfort, geen noodzaak. Los oppakken, niet meeliften op ander werk. **Stap 1 — invullen (FlutterFlow-property, laag risico).** Pagina `login`, beide `TextFormField`s: | veld | herkenbaar aan | Auto Fill Hint | |---|---|---| | gebruikersnaam | `obscureText: false`, regel ~227 in de export | `username` | | wachtwoord | `obscureText: !_model.passwordFieldVisibility`, regel ~402 | `password` | Zit in **Properties Panel → Additional Properties → Auto Fill Hint** (toggle aan, dan de optie kiezen). Kies `username` en **niet** `email`: Drupal 7's `user/login` accepteert standaard alleen de gebruikersnaam. **Stap 2 — opslaan aanbieden (custom action, hier zit het risico).** Alleen hints is niet genoeg: het OS vraagt pas "wachtwoord opslaan?" na `TextInput.finishAutofillContext()`. FlutterFlow genereert die aanroep niet en documenteert hem niet. Nieuwe custom action `autofillAfronden`, zonder argumenten, aan te hangen **ná** een geslaagde `drupalLogin`: ```dart import 'package:flutter/services.dart'; Future autofillAfronden() async { TextInput.finishAutofillContext(); } ``` **Open vraag:** werkt dit zonder een `AutofillGroup` om beide velden heen? FlutterFlow genereert die niet zichtbaar, en Android is daar in Flutter historisch kieskeurig in. Lukt het niet, dan is de enige route een custom widget dat het hele loginformulier vervangt — dat is een flinke ingreep en waarschijnlijk niet de moeite waard voor puur comfort. **⚠️ Testen kan NIET op de emulators.** De tablet-AVD heet `Medium_Tablet_no-google`: geen Play Services, dus geen Google Password Manager. Dit moet op Bob's fysieke Xiaomi, met een profile-build. **Terugdraaien** (per stap, beide onafhankelijk): 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/` | 200 op drie echte toekomstige events, landt op de juiste eventpagina | | dichtgezet API-oppervlak | `node` · `file` · `taxonomy_term` · `system/connect` · `user/` 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-**label**tekst 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=` 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** | 🟡 **Werkt op devbob** (2026-09-21) | 4/4 tests groen: callback rendert, callback→claim vinden elkaar via de cache, een state is eenmalig, ongeldige state geeft 400. **Nog te doen:** de bijgewerkte `.inc` + de tweede `hook_menu`-regel (pad-variant) plaatsen, en daarna alles naar **productie** — de app praat met uitgaanskrant.com | | **130-G** | ✅ **Code klaar** (2026-09-21), nog niet in de builder | `socialLoginStart(provider)` — state genereren, Custom Tab openen (`LaunchMode.inAppBrowserView`, **niet** `inAppWebView`), elke 2 s claimen, App State vullen als `drupalLogin`, tab sluiten met `closeInAppWebView()`. Timeout 3 min. `dart analyze` 0 errors. Wacht op 130-F. ⚠️ Te verifiëren op een toestel: of Android de polling niet throttlet terwijl de Custom Tab voorgrond is — zo ja, claimt hij alsnog bij terugkeer, want de state is 5 min geldig | | **130-H** | 📋 **Draaiboek klaar** (2026-09-21) | Twee knoppen onder de bestaande Inloggen-knop, gemaakt door die knop te dupliceren (erft stijl). Facebook `#1877F2`/wit, Google wit met rand `#DADCE0`. Per knop een **unieke** Action Output Variable Name, anders dubbele declaratie in het model. Stappen in `130-app-draaiboek.md` | | **130-I** | 📋 **Teksten klaar** (2026-09-21) | *Log in with Facebook* / *Log in with Google*, via het globe-icoontje. ⚠️ Een gedupliceerde knop erft de Engelse vertaling van het origineel, dus dit hoort bij 130-H, niet erna | | **130-J** | Bob | Echte doorloop met een Facebook- én een Google-account op een profile-build. Claude kan dit niet — geen wachtwoorden | | **130-K** | ✅ **Tekst klaar** (2026-09-21), Bob voert door | Drie wijzigingen in `/nl/support/privacybeleid` (sectie 1a account, sectie 3 derden met OneAll+Meta+Google, datum/draft) + in Data Safety komt **Personal info → Name** erbij als *Collected*. Volledige tekst in `130-K-privacy.md`. ⚠️ **Losse bug daarbij gevonden: de privacylink in de cookiebanner geeft 404** (`/nl/support/algvoorwaarden`, moet `/nl/support/algemenevoorwaarden` zijn) — Play eist een werkende privacy-URL, dus dit is sowieso repareren op `/admin/config/system/eu-cookie-compliance` | | **130-L** | Claude | Bevindingen naar `CLAUDE.md` | **⏰ HERINNERING VOOR 2026-09-22 (Bob vroeg hier expliciet om):** `devbob.uitgaanskrant.com` toevoegen aan de **allowed domains** in het OneAll-dashboard. Kon op 2026-09-21 niet — devbob had een storing. Zonder dat domein weigert OneAll de `callback_uri` en is testen op devbob onmogelijk. **Stand 2026-09-21, tweede testronde — backend is KLAAR:** Beide callback-routes bewezen op devbob: `/callback?state=X` én `/callback/X`, en in beide gevallen vindt de claim de state terug (`{"status":"error","message":"cancelled"}`), geeft een tweede claim weer `pending` en een te korte state een 400. Er valt aan de Drupal-kant niets meer te testen tot het OneAll-abonnement omgaat. Rest: de aangepaste `.inc` + het tweede `hook_menu`-item nog naar **productie** (de oude versie staat er al). **Stand 2026-09-21 na de eerste testronde:** - ⚠️ **De upgrade is nog niet actief** — gemeten: Direct Connect geeft nog steeds *"not available in the Starter plan"*. - **Devbob gebruikt hetzelfde OneAll-subdomein** (`uitgaanskrantcom`) als productie. Testen op devbob kan dus, maar dan moet `devbob.uitgaanskrant.com` bij de **allowed domains** staan (Advanced geeft er 8). - **De state gaat door het PAD, niet door de query** (`/nl/app-social-login/callback/`). Het is niet bewezen dat OneAll een bestaande query-parameter in de `callback_uri` laat staan als hij er zelf `?connection_token=` aan toevoegt; met de state in het pad kan dat niet misgaan. De query-variant blijft bestaan als terugval en om handmatig te testen — daarom staan er **twee** `hook_menu`-items. **Alles wat zonder het abonnement kon, is af (2026-09-21).** Backend getest, app-code geschreven, privacytekst klaar, bouwdraaiboek klaar. Openstaand tot de upgrade: alleen 130-A, en daarna G t/m J in één sessie. ⚠️ **Besluit Bob 2026-09-21: Claude werkt pas in FlutterFlow zodra het abonnement is aangepast.** Tot die tijd alleen voorbereiden. 💡 **De app-keten is te testen zonder de upgrade**: vervang in `socialLoginStart` de `startUrl` tijdelijk door `callbackUri`, dan slaat hij OneAll over en loopt de rest van de keten wél (browser, polling, foutafhandeling, tab sluiten). Zie het draaiboek. **Volgorde:** A t/m C zijn besluiten en blokkeren D. F blokkeert G. K moet af vóór de Play-inzending als 130 in de eerste release meegaat.