Просмотр исходного кода

Sessieduur gedocumenteerd (365 dagen) + taak 115 autofill

- CLAUDE.md: login-sessie staat sinds 2026-09-20 op een jaar
  (local.settings.php). Vastgelegd dat alleen gc_maxlifetime telt voor de app,
  dat de teller loopt vanaf de laatste call MET cookie, en welke calls dat zijn.
  Plus: session_api/session_cache hebben niets met inloggen te maken.
- CLAUDE.md: ssh-host gecorrigeerd naar de alias aegir-o1; aegir1:959 is
  IPv6-only en werkt niet vanaf Bob's desktop.
- CLAUDE.md: 403-detectie in drupalRequest + sessiemelding in VerbindingsBanner
  beschreven (geimplementeerd en geverifieerd, 0 analysefouten).
- TASKS.md: taak 115 voor autofill, met terugdraai-instructies per stap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bob 19 часов назад
Родитель
Сommit
77021e36c3
2 измененных файлов с 211 добавлено и 98 удалено
  1. 48 1
      CLAUDE.md
  2. 163 97
      TASKS.md

+ 48 - 1
CLAUDE.md

@@ -5134,12 +5134,20 @@ alleen waren het de verkeerde. **Bouw daarom in elk beheerscript een
 "HUIDIGE STAND"-kop in die per doelwit `<-- FASE N` toont**, dan zie je in de
 "HUIDIGE STAND"-kop in die per doelwit `<-- FASE N` toont**, dan zie je in de
 droogdraai meteen of je het juiste script voor je hebt. Overzetten:
 droogdraai meteen of je het juiste script voor je hebt. Overzetten:
 ```
 ```
-scp -P 959 <script> o1@aegir1.digitalforce.nl:~/tmp/
+scp <script> aegir-o1:~/tmp/
 drush @<alias> php-script ~/tmp/<script>        # droogdraai als default
 drush @<alias> php-script ~/tmp/<script>        # droogdraai als default
 drush @<alias> php-script ~/tmp/<script> doen   # uitvoeren
 drush @<alias> php-script ~/tmp/<script> doen   # uitvoeren
 ```
 ```
 Aliassen: `@uitgaanskrant.com` (productie) en `@devbob.uitgaanskrant.com`.
 Aliassen: `@uitgaanskrant.com` (productie) en `@devbob.uitgaanskrant.com`.
 
 
+**⚠️ De host heet `aegir-o1` (ssh-alias), niet `aegir1.digitalforce.nl:959`.** Die
+laatste — zoals hier eerder stond — resolvet alleen naar IPv6 en Bob's desktop heeft
+geen IPv6-route, dus je krijgt `Network is unreachable`; forceer je IPv4 dan geeft
+poort 959 `Connection refused`. De werkende aliassen staan in
+`~/.ssh/config.d/aegir.conf`: **`aegir-o1`** (drush-gebruiker, dít is de juiste) en
+`aegir` (root). Claude kan daar dus gewoon zelf lezen — `ssh aegir-o1 "drush @uitgaanskrant.com php-eval ..."`
+werkt, en dat scheelt een ronde heen-en-weer met Bob voor elke meting.
+
 **Meet bij het uitzetten van API-oppervlak ook wat er moet BLIJVEN werken — en
 **Meet bij het uitzetten van API-oppervlak ook wat er moet BLIJVEN werken — en
 meet dat op devbob vóór productie.** Alleen "geeft het nu 404?" is een halve
 meet dat op devbob vóór productie.** Alleen "geeft het nu 404?" is een halve
 check; de vraag is of de app nog draait. Vaste regressieset voor dit project:
 check; de vraag is of de app nog draait. Vaste regressieset voor dit project:
@@ -5209,6 +5217,45 @@ de inlognaam van de auteur, dus user enumeration op al je redacteuren.
   checken), anders lijkt de UI kapot terwijl het eigenlijk gewoon de
   checken), anders lijkt de UI kapot terwijl het eigenlijk gewoon de
   ontbrekende Engelse vertaling is.
   ontbrekende Engelse vertaling is.
 
 
+- **Een login-sessie is sinds 2026-09-20 een JAAR geldig (was 28,5 dagen).**
+  Ingesteld in `local.settings.php` van de site (dat bestand wordt als láátste
+  ingeladen en overleeft een Aegir verify; `settings.php` niet):
+  `ini_set('session.gc_maxlifetime', 31536000)` + `session.cookie_lifetime` idem.
+  Meet 'm met
+  `ssh aegir-o1 "drush @uitgaanskrant.com php-eval \"echo ini_get('session.gc_maxlifetime')/86400;\""`.
+  - **Voor de app telt alleen `gc_maxlifetime`.** De app stuurt de cookie met de
+    hand mee uit secure storage (`Cookie: SESSxxx=sessid`), er is geen browser die
+    op een vervaldatum let — `cookie_lifetime` doet dus alleen iets voor de website.
+  - **De teller loopt vanaf de laatste geauthenticeerde request, niet vanaf het
+    inloggen.** `_drupal_session_write()` in `includes/session.inc` schrijft
+    `timestamp = REQUEST_TIME` bij elke request die >180 s (`session_write_interval`)
+    na de vorige komt. **Alleen de cookie is nodig, geen wachtwoord** — er hoeft dus
+    niets "ingebouwd" te worden om een sessie te verlengen.
+  - **Maar bladeren verlengt niks:** Home, de overzichten en evenement-detail gaan
+    anoniem. Alleen deze sturen de cookie mee, en verlengen dus:
+    `drupalRequest`, `evenementCreate`, `bestandUpload`, `stadsactiviteitCreate`,
+    en de API Calls `Categorieen` / `Entreeopties` / `MijnHorecagelegenheden` /
+    `MijnStadsrechten`. In de praktijk: Favorieten, Mijn Profiel, een hartje,
+    gemeente volgen.
+  - **`_drupal_session_read()` doet `WHERE s.sid = :sid` zónder timestamp-check**,
+    dus een formeel verlopen sessie werkt gewoon door tot garbage collection hem
+    opruimt — en die draait met 1 kans op 1000 per request.
+  - ⚠️ **`session_api` en `session_cache` hebben NIETS met inloggen te maken**, hoe
+    veelbelovend hun instellingenscherm ook oogt. Dat zijn eigen tracking-cookies
+    voor álle bezoekers (`Drupal.session_cache.sid`, 120 dagen; `session_api_session`,
+    30 dagen). Hun variabelen staan bovendien alle drie op `NULL` in de
+    variabelentabel — je ziet daar fabriekswaarden, geen gemaakte keuze. Ga dus niet
+    aan die schermen sleutelen om de logingeldigheid te veranderen.
+- **Een verlopen sessie logt de app sinds 2026-09-20 netjes uit.** `drupalRequest`
+  herkent 401, of 403 mét het woord `anonymous` ("Toegang geweigerd voor gebruiker
+  anonymous"), **maar alleen als er daadwerkelijk een cookie meeging** — een 403
+  zonder dat woord is een rechtenkwestie en mag niemand uitloggen. Hij wist dan
+  dezelfde zes App State-velden als de uitlogknop, waardoor alle bestaande
+  "ben je ingelogd?"-condities vanzelf omklappen naar hun uitgelogde variant, en zet
+  de niet-persisted boolean `sessieVerlopen`. `VerbindingsBanner` (staat al in
+  `HeaderButtonsComponent`, dus op 11 pagina's) toont daarop een eenmalige SnackBar
+  met een knop naar Login. De returnwaarde blijft bewust `[]` — aanroepers doen blind
+  `.toList()`.
 - **Drupal 7 Services sessie-auth**: `sessid` + `session_name` (uit
 - **Drupal 7 Services sessie-auth**: `sessid` + `session_name` (uit
   `LoginCall`'s response) vormen samen de sessie-cookie:
   `LoginCall`'s response) vormen samen de sessie-cookie:
   `Cookie: <session_name>=<sessid>`. `token` is een los CSRF-token,
   `Cookie: <session_name>=<sessid>`. `token` is een los CSRF-token,

+ 163 - 97
TASKS.md

@@ -44,10 +44,10 @@ Google Play en één doorloop die alleen Bob kan doen.
 | **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 |
 | **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 |
 | **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 |
 | **Bij livegang** | Schakelaars en opruimklussen | Eigen blok hieronder |
-| **115** | AdBanner verplaatsen op `PUitgaanPage` | Onder de slider, bóven zoekveld/datum/chips. Eigen blok hieronder |
-| **116** | AdBanner springt bij het laden | Oorzaak gevonden in de gegenereerde widget; fix is een vaste hoogte. Eigen blok hieronder |
-| **117** | Kalenderpermissie eruit | De app heeft 'm niet nodig; scheelt een permissie in de Play-listing. Eigen blok hieronder |
-| **118** | AdMob EU-consentbericht | De app roept UMP al aan, maar het bericht moet in de AdMob-console gepubliceerd zijn. Eigen blok hieronder |
+| ~~115~~ | ~~AdBanner verplaatsen~~ | ✅ **AF** (Bob 2026-09-20, export-geverifieerd): volgorde is nu slider → banner → filterbalk → lijst |
+| **116** | AdBanner springt bij het laden | ✅ Container + responsive hoogte staat goed. ⚠️ Eén restpunt: de oude `Padding(top 15)` zit nú **binnen** die Container en eet 15 van de 50 dp |
+| ~~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 |
 | **119** | Besluit: App Links voor `https://` | Gedeelde links openen nu altijd de browser, nooit de app. Post-launch keuze |
 | **119** | Besluit: App Links voor `https://` | Gedeelde links openen nu altijd de browser, nooit de app. Post-launch keuze |
 
 
 ---
 ---
@@ -251,88 +251,45 @@ bij inschrijving.
 **Route wanneer het zover is:** FlutterFlow kan ook voor iOS bouwen, dus een Mac
 **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.
 is niet strikt nodig. Punt 5 en 6 zijn het echte werk; de rest is administratie.
 
 
-### 115 · AdBanner verplaatsen op `PUitgaanPage` · Eigenaar: Bob
-
-**Wens (Bob 2026-09-20):** de banner onder de slider en **boven** het zoekveld,
-de datumkiezer en de choicechips.
-
-**Stand nu** (verse export 2026-09-20, `lib/uitgaanspaginas/p_uitgaan_page/p_uitgaan_page_widget.dart:164-230`),
-body-`Column` met vier kinderen:
-
-1. `PUitgaanSliderComponent` — Visibility `!FFAppState().zoekOpen`
-2. `Expanded > FilterBalkComponent` — zoekveld, datum, chips
-3. `Padding(top 15) > FlutterFlowAdBanner` ← moet naar plek 2
-4. `Expanded > Padding > Container > PUitgaantabelKaartComponent`
-
-**Route: twee sleepacties.** Een drop op een container-rij landt altijd op
-**positie 0**, dus in omgekeerde volgorde slepen:
-
-1. Klap alle vier de kinderen in (chevrons), selecteer de body-`Column`-rij één
-   keer en druk `Escape` — een sleep landt alleen als de doelrij vooraf
-   geselecteerd is.
-2. Sleep de **`Padding`-rij met de AdBanner** op de **`Column`-rij**
-   → `[banner, slider, filterbalk, lijst]`.
-3. Sleep de **slider-rij** op de **`Column`-rij**
-   → `[slider, banner, filterbalk, lijst]` ✅
-
-Eén sleep per keer, met een verse `zoom` op de boom ertussen (de rijen
-verschuiven). De Visibility-conditie op de slider overleeft een sleep.
-
-**Let op:** de twee horeca-overzichtspagina's hebben hun banner **onderaan** de
-Column staan. Die is niet meeverplaatst — apart besluit als je ze gelijk wilt
-trekken.
-
-### 116 · AdBanner "opent groot en wordt dan klein" · Eigenaar: Bob
-
-**Geen AdMob-kwestie — het zit in `lib/flutter_flow/flutter_flow_ad_banner.dart`
-(gegenereerd, dus niet lokaal te repareren).** Vóór het laden rendert die widget
-een zwarte `Container` **zonder eigen hoogte** met een laadtekst erin; die tekst
-is extra lang zolang `showsTestAd: true` (er komt een waarschuwingsalinea bij).
-Op 411 dp is dat ruwweg 150 dp hoog. Zodra de banner binnen is wordt de Container
-exact zo hoog als de banner (~50 dp). Vandaar de sprong — en de `Expanded`-blokken
-eromheen krimpen en groeien mee, dus de hele lijst verspringt.
-
-Twee dingen volgen daaruit: het wordt **vanzelf minder** zodra *Show Test Ads*
-uit gaat (kortere laadtekst), en het verdwijnt **niet** vanzelf — bij een
-uitblijvende fill blijft die placeholder gewoon staan.
-
-**Fix: geef de banner een gereserveerde hoogte.**
-- Rechtsklik de AdBanner → **Wrap Widget → Container**, geen Fill Color.
-- Op die Container **Height** als **Responsive Value**: `50 / 50 / 90 / 90`.
-  Dat volgt Google's anchored-adaptive-hoogtes (<400 dp → 32, 400-720 → 50,
-  >720 → 90); op een tablet wil de banner dus echt 90.
-- **Ad Banner Dimensions leeg laten** (= adaptive). Dat is bewust anders dan de
-  twee horeca-pagina's, die `width: schermbreedte, height: 50` hebben staan: dat
-  is een *custom* AdSize en die vult AdMob beduidend slechter dan een
-  standaard/adaptive formaat. Nameten zodra er echte advertenties draaien.
-
-**Terugvaloptie als de Responsive-Value-dialoog tegenwerkt:** vaste Height 50 en
-accepteren dat de tabletbanner 40 dp krapper staat dan hij wil.
-
-### 117 · Kalenderpermissie uit het manifest · Eigenaar: Bob
-
-**De app vraagt `READ_CALENDAR` en `WRITE_CALENDAR` aan zonder ze te gebruiken.**
-Nagemeten 2026-09-20:
-- `zet_in_agenda.dart:51` gebruikt `Add2Calendar.addEvent2Cal(...)`. Dat is de
-  **intent**-route (`ACTION_INSERT`) en die vraagt het toestel om de agenda-app te
-  openen — daar is geen enkele permissie voor nodig.
-- De plugin declareert ze zélf niet: `~/.pub-cache/.../add_2_calendar-3.0.1/android/
-  src/main/AndroidManifest.xml` is een **leeg** manifest.
-- Ze komen dus uit **App Settings → Permissions** (aangezet bij taak 98) en staan
-  in de export op `android/app/src/main/AndroidManifest.xml:8-9`.
-
-**Waarom het uitmaakt:** Play toont "Agenda" bij de app-permissies, en dat is een
-van de dingen waar mensen op afhaken bij een uitgaansapp. Bovendien scheelt het een
-rij in het Data safety-formulier (G2.7).
-
-**Doen:** App Settings → Permissions → **Calendar uit** → verse export → de twee
-regels moeten uit het manifest zijn → daarna de agendaknop op `EventCurrent` één
-keer testen op een echt toestel (de AVD's kunnen dit niet, hun AOSP-kalender is
-kapot — zie `CLAUDE.md`).
-
-⚠️ **Bij de iOS-ronde terugzetten.** Op iOS gebruikt `add_2_calendar` EventKit en
-daar is de permissie + `NSCalendarsUsageDescription` wél nodig. Nu Android-only is
-dat geen bezwaar.
+### ~~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 — ⚠️ één restpunt · Eigenaar: Bob
+
+**Gedaan en geverifieerd:** de `Container` staat er met `height` als Responsive
+Value `50 / 50 / 90 / 90`, en de AdBanner heeft **geen** eigen width/height meer
+(dus adaptive, zoals bedoeld). De layout springt hiermee niet meer.
+
+⚠️ **Restpunt dat ik over het hoofd zag toen ik het recept gaf:** de oude
+`Padding(top: 15)` die om de banner zat, zit nu **binnen** die nieuwe Container.
+De Container geeft 50 dp, de padding pakt daar 15 van af, dus de banner houdt er
+**35** over terwijl hij er 50 wil. Constraints winnen van een gewenste hoogte, dus
+de advertentie wordt aan de onderkant afgeknepen — zonder foutmelding, en met test
+ads makkelijk over het hoofd te zien.
+
+**Kies er één:**
+- **Padding op 0** (de banner staat nu toch al los onder de slider), of
+- **hoogte op `65 / 65 / 105 / 105`** als je die 15 dp witruimte erboven wilt houden.
+
+🔸 Los daarvan blijft de zwarte laadtekst van `FlutterFlowAdBanner` hoger dan de
+Container en tekent dus even buiten zijn kader. Dat is inherent aan de gegenereerde
+widget en niet vanuit de builder te clippen; met test ads laadt hij snel genoeg om
+het niet te merken. De layout zelf beweegt niet meer, en dat was het doel.
+
+### ~~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
 ### 118 · AdMob: is het EU-consentbericht gepubliceerd? · Eigenaar: Bob
 
 
@@ -349,20 +306,72 @@ advertenties (lagere opbrengst), en Google kan de serving beperken.
 voor de app en op **Gepubliceerd** zetten. Daarna bij de eerste release-test
 voor de app en op **Gepubliceerd** zetten. Daarna bij de eerste release-test
 controleren dat het scherm ook echt verschijnt bij een verse installatie.
 controleren dat het scherm ook echt verschijnt bij een verse installatie.
 
 
-### 119 · Besluit: App Links voor `https://uitgaanskrant.com` · Eigenaar: Bob
+⚠️ **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.
+
+### 119 · App Links — ontwerp na Bob's vraag van 2026-09-20 · Eigenaar: Bob
 
 
-Het manifest heeft één intent-filter, op het **custom scheme**
-`uitgaanskrant://uitgaanskrant.com` (`AndroidManifest.xml:47-52`). Er is géén
-filter voor `https://`. Gevolg: een uitgaanskrant.com-link die iemand in WhatsApp
-of een mail aantikt opent **altijd de browser**, nooit de app — ook de link die de
-deelknop zelf verstuurt (`https://uitgk.com/<nid>`).
+**Bob's vraag:** "ik kan een lijstje domeinen opgeven — maar hoe weet mijn app wat
+de URL is? een evenement, een horecagelegenheid, een stadspagina?"
 
 
-Dat is nu geen fout, maar wel een gemiste kans zodra de app live staat. Het vergt
-een `assetlinks.json` op de domeinen plus een tweede intent-filter, en dus een
-bewust besluit. **Post-launch, geen blokker.**
+**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.
 
 
-(Terzijde: `android:autoVerify="true"` staat nu op het custom-scheme-filter en doet
-daar niets — autoVerify geldt alleen voor http/https. Onschadelijk.)
+**Daarom NIET `uitgaanskrant.com` registreren.** Twee redenen:
+1. De site-URL's zijn SEO-aliassen zonder nid en zonder type
+   (`/nl/Noord-Holland/Amsterdam/Bimhuis-Amsterdam`). De app kan daar niets mee
+   zonder Drupal om een vertaling te vragen — een extra endpoint erbij.
+2. Je zou **élke** link naar je site de app in trekken: privacybeleid,
+   registratieformulier, zoekpagina's, de hele CMS-kant. Voor alles wat de app niet
+   kent moet je dan een nette terugval bouwen.
+
+**Wel `uitgk.com`.** Dat is jouw eigen korte domein, het wordt alleen gebruikt door
+de deelknop (`event_current_widget.dart:414`) en door `zetInAgenda`
+(`zet_in_agenda.dart:42`), en het heeft één voorspelbare vorm: `/<nid>`.
+
+**Het type is met één API-call te bepalen — gemeten 2026-09-20:**
+
+| | `flutterflow_events.json?nid=` | `flutterflowmobiel_establishment_info.json?nid=` |
+|---|---|---|
+| **222338** (event) | 1 rij, mét `horecagelegenheidnid=75459` | 0 rijen |
+| **75459** (zaak) | 0 rijen | 1 rij |
+
+Dus: één call naar `flutterflow_events.json?nid=X`; komt er een rij uit, dan is het
+een event **en heb je de `horecaid` meteen te pakken** (die heeft `EventCurrent`
+als tweede parameter nodig). Nul rijen → behandel als zaak →
+`HorecagelegenheidCurrent(nid: X)`. Allebei leeg → Home.
+
+**Stadspagina's erbuiten laten.** `PUitgaanPage` wil een `plaats`-tid, en die staat
+in geen enkele URL — dat zou een derde lookup via `plaatsen.json` vergen voor
+weinig winst.
+
+**Twee routes om uit te kiezen:**
+- **A — deel-URL kort houden (`uitgk.com/222338`) + een routerpagina in de app.**
+  Mooiste links. Vergt een pagina met een pad-parameter op rootniveau (`/:nid`),
+  de API-call hierboven en een conditionele navigatie. ⚠️ **Open vraag: kan
+  FlutterFlow een routePath `/:nid` op rootniveau aan zonder te botsen met `/home`,
+  `/favorieten` enz.?** go_router matcht exacte paden eerst, dus het zou moeten
+  kunnen — maar dat is niet gemeten. Eén testpagina + een export geeft uitsluitsel.
+- **B — deel-URL wordt `uitgk.com/eventCurrent?nid=..&horecaid=..`.** Dan matcht
+  go_router het pad meteen en hoeft er in de app **niets** gebouwd te worden (het
+  custom scheme werkt al precies zo). Prijs: lange, lelijke deellinks, plus een
+  redirect-regel op uitgk.com voor wie de app niet heeft.
+
+**Volgorde-afhankelijkheid, hierom is dit hoe dan ook post-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.
 
 
 ### 65 · Release-build — wat er NOG getest moet worden · Eigenaar: Bob
 ### 65 · Release-build — wat er NOG getest moet worden · Eigenaar: Bob
 
 
@@ -648,6 +657,63 @@ werkelijkheid gewoon events van vandaag waren die al begonnen waren — precies
 het `>= -2 uur`-filter hoort weg te laten. Staat ook al in `CLAUDE.md`; ik trapte
 het `>= -2 uur`-filter hoort weg te laten. Staat ook al in `CLAUDE.md`; ik trapte
 er alsnog in.
 er alsnog in.
 
 
+### 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
 ## 📚 Referentie — geen taak, wél lezen vóór je iets opnieuw onderzoekt
 
 
 #
 #