feat(push): VAPID Web Push voor queue-berichten #81

Merged
janpeter merged 28 commits from feat/vapid-queue-push into main 2026-08-10 19:28:53 +02:00
Owner

OS-niveau notificaties op Mac en iPhone wanneer een s4m-queue-bericht binnenkomt of van status wisselt, ook met gesloten tab of app.

NIET MERGEN tot het testverslag erin staat. De deploy draait bewust vanaf deze branch, zodat de commits van taak 14/15 (AC1-AC9-bewijs) nog in dezelfde merge mee kunnen. Zou deze PR nu gemerged worden, dan bereikt dat verslag main nooit.

Wat erin zit:

  • lib/push/payload.ts — envelope naar notificatie; nieuw bericht = previous_status === null (een requeue is dus geen nieuw bericht)
  • lib/push/server.ts — verzenden, filter op app_scope=workers + rol ADMIN, en sendOne geeft de HTTP-status terug: dat is de bron van het 201-bewijs
  • lib/push/queue-listener.ts + instrumentation.ts — persistente LISTEN met advisory lock, server-side keepalives en per message-id geserialiseerde sends
  • DockerfileCOPY instrumentation.ts ./ vóór de build; zonder die regel zit de listener niet in de image terwijl de UI gewoon werkt
  • actions/push.ts, public/sw.js, lib/push/client.ts, components/push-toggle.tsx, proxy.ts (/sw.js in PUBLIC_PATHS), app/layout.tsx (de Toaster ontbrak volledig)

Ontwerp: docs/superpowers/specs/2026-08-10-vapid-queue-push-notifications-design.md (vijf reviewrondes, dubbele GO).
Plan: docs/superpowers/plans/2026-08-10-vapid-queue-push-notifications.md.

Voorwaarden uit de app_scope-keten zijn afgerond: scrum4me-shared #46 en Scrum4Me #146 gemerged, migratie toegepast en geverifieerd.

OS-niveau notificaties op Mac en iPhone wanneer een s4m-queue-bericht binnenkomt of van status wisselt, ook met gesloten tab of app. **NIET MERGEN tot het testverslag erin staat.** De deploy draait bewust vanaf deze branch, zodat de commits van taak 14/15 (AC1-AC9-bewijs) nog in dezelfde merge mee kunnen. Zou deze PR nu gemerged worden, dan bereikt dat verslag `main` nooit. Wat erin zit: - `lib/push/payload.ts` — envelope naar notificatie; nieuw bericht = `previous_status === null` (een requeue is dus geen nieuw bericht) - `lib/push/server.ts` — verzenden, filter op `app_scope=workers` + rol ADMIN, en `sendOne` geeft de HTTP-status terug: dat is de bron van het 201-bewijs - `lib/push/queue-listener.ts` + `instrumentation.ts` — persistente LISTEN met advisory lock, server-side keepalives en per message-id geserialiseerde sends - `Dockerfile` — `COPY instrumentation.ts ./` vóór de build; zonder die regel zit de listener niet in de image terwijl de UI gewoon werkt - `actions/push.ts`, `public/sw.js`, `lib/push/client.ts`, `components/push-toggle.tsx`, `proxy.ts` (`/sw.js` in PUBLIC_PATHS), `app/layout.tsx` (de Toaster ontbrak volledig) Ontwerp: `docs/superpowers/specs/2026-08-10-vapid-queue-push-notifications-design.md` (vijf reviewrondes, dubbele GO). Plan: `docs/superpowers/plans/2026-08-10-vapid-queue-push-notifications.md`. Voorwaarden uit de app_scope-keten zijn afgerond: scrum4me-shared #46 en Scrum4Me #146 gemerged, migratie toegepast en geverifieerd.
Aanpak A (listener + subscribe-UI in workers-app), hergebruik
push_subscriptions met één gedeeld VAPID-keypair, alle envelopes
met tag-dedup, bewijsstrategie in vier lagen (AC1-AC6).
Afgestemd met JP 2026-08-10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correcties vóór ronde 1 van de review-loop: tabelnaam is agent_message
(lib/queue/ops-db.ts), OPS_DATABASE_URL vs DATABASE_URL expliciet,
prisma/schema.prisma is gegenereerd en niet in git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Geverifieerd dat elke queue-schrijver NOTIFY emit (CLI src/db.ts, MCP
claim.ts/queue-push/done/fail, workers ops-db.ts) met file:line. Twee
gaten vastgelegd: best-effort NOTIFY bij queue_push mist de 5s-poll
safety net die een push-listener niet heeft, en S4M_QUEUE_CHANNEL-drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Beide reviewers vonden onafhankelijk dezelfde BLOCKER: NEXT_PUBLIC_* wordt
op build-tijd ge-inline terwijl de workers-image bouwt voor de runtime-env
bestaat. Publieke sleutel gaat nu via een server component als prop.

Verder: COPY instrumentation.ts in de Dockerfile (anders geen listener in
de image), app_scope-kolom + eigen keypair per app tegen cross-app fan-out
(besluit 2 herzien naar 2', vereist JP-akkoord wegens schemaketen),
advisory lock tegen dubbele sends, per-id geserialiseerde sends,
keepalive op de LISTEN-verbinding, AC5 verzwaard, AC7/AC8 toegevoegd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Geen BLOCKERs meer. Codex' MAJORs gingen over beloftes zonder afdwinging:
AC4 kon niet slagen omdat done --reply twee envelopes emit (gedrag is
gewenst, de assertie was fout), de volgorde-keten sloot de async
snippet-lookup uit, het geeiste 201-bewijs had geen bron omdat sendOne
de response weggooit, en 'geen migratie nodig' sprak besluit 2' tegen.

Kimi (GO) vond o.a. dat het advisory lock zelf een stiltemodus kan
worden bij een half-open verbinding, en dat de header ten onrechte
JP-akkoord claimde op de opslagkeuze die 2' juist herziet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Het antwoord keert de route om (from = verzoek.to), dus AC4 verwachtte
de verkeerde titel. Dat legde een echt gat bloot: de melding waar JP op
wacht ('heb ik antwoord?') werd door geen enkel criterium geraakt.
Mapping benoemt nu beide spiegelflows; AC9 toegevoegd voor de
JP-naar-agent-flow.

Verder: /sw.js toevoegen aan proxy.ts PUBLIC_PATHS (deny-by-default
whitelist), en to_model='jp' i.p.v. hardcoded mac:jp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checklist besluit 2' miste de Scrum4Me-submodule-bump: die app genereert
zijn schema uit zijn eigen gepinde vendor-kopie (gitlink f78a458, geen
app_scope), dus het filter was niet te schrijven. Nu vijf stappen plus
expliciete deployvolgorde.

Verder: derde dekkingsgat (CLI-claim is niet transactioneel), proxytest
en het actieve PWA-productdoc als deliverables (dat doc zegt nu nog dat
workers geen service worker heeft), citaties naar symbolen, en
'pending-pad' vervangen door 'previous_status = null'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codex 0/0/2 MINOR GO en kimi 0/0/2 MINOR GO op r7. Trend over vijf
rondes: 2 BLOCKER + 4 MAJOR -> 0+4 -> 0+1 -> 0+1 -> 0+0. 33 findings,
allemaal geverifieerd en geaccepteerd, geen enkele afgewezen.

Laatste MINORs verwerkt: 'alle vier' -> 'alle vijf' (beide reviewers),
mappingtabel draagt nu de expliciete conditie previous_status = null
zodat een requeue niet als nieuw bericht wordt gelezen, en
.env.example + docs/pages/queue-messages.md staan als deliverable in
de componententabel.

Blokkerend open punt voor JP: besluit 2' (app_scope-schemawijziging
over drie repos) gaat verder dan wat in de brainstorm is goedgekeurd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Geen blokkerend open punt meer; de vijfstaps-schemaketen wordt fase 1
van het implementatieplan. Plan-fase kan starten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fase A: de app_scope-schemaketen over scrum4me-shared, Scrum4Me en de
migrator. Fase B: verzendkant (mapping, sender, listener met advisory
lock). Fase C: aanmeldkant (actions, service worker, toggle). Fase D:
configuratie, docs en het bewijs voor AC1-AC9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BLOCKER: het productiebewijs stond voor een niet-bestaande uitrol. Nieuwe
taak 13 zet branches, PR's, deploy-volgorde en de sleutels op de server;
de bewijstaken schuiven naar 14 en 15.

Verder: single-flight reconnect met generatie-guard en afgevangen
timer-rejections (plus twee lifecycle-tests), Prisma 7.8 kent geen
--shadow-database-url en db execute geeft geen rijen terug (en psql
staat niet eens op het PATH), log-hygienetest met sentinels voor elk
geheim veld, Scrum4Me-branch wordt nu gepusht, en twee snippet-fixes
van kimi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uitrol liep via SSH terwijl het vastgelegde contract een s4m-queue-task
naar scrum4me-server:claude is (host niet SSH-bereikbaar); taak 13 stap 6
is nu die task met exacte paden en verplicht bewijs in de reply.

Beide lifecycle-tests uit r2 waren kapot: de eerste kon niet slagen tegen
zijn eigen implementatie, de tweede was leeg door een statische import.
Herschreven met dynamische import en een keepalive die op de SQL faalt.

Verder: node --env-file voor de pg-checks (DATABASE_URL staat alleen in
Scrum4Me/.env.local), workers-PR pas mergen in taak 15 zodat het
testverslag main haalt, sentinel-test met verse module zodat
module-init-logging betrapt wordt, AC7 telt nu de aggregatieregel in
plaats van de per-subscription regels, en een Toaster in de layout —
zonder die is elke toggle-foutmelding onzichtbaar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sluitstuk van de schemaketen: workers kent nu de app_scope-kolom in zijn
gegenereerde Prisma-client. web-push is de verzendbibliotheek voor de
listener uit fase B.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nieuw bericht wordt bepaald door previous_status === null, niet door
status === 'pending' — een requeue is geen nieuw bericht en krijgt dus
geen snippet-lookup. Adressering matcht op to_model omdat jp op elke
server bestaat, en de route keert om bij done, waardoor jp's eigen
antwoord de generieke titel krijgt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sendOne geeft de HTTP-status terug in plaats van hem weg te gooien; dat
is de bron van het 201-bewijs in de acceptatiecriteria. Logregels dragen
alleen message-id en status — de sentinel-test bewaakt dat endpoint,
sleutels, payloadvelden en VAPID-sleutels nergens in een logregel komen.

De admin-rol wordt bij het verzenden opnieuw gecontroleerd, niet alleen
bij het aanmelden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De hele bewerking wordt per message-id geketend, inclusief de
snippet-lookup: alleen sends ketenen laat een trage lookup zijn melding
na 'done' inschuiven en met dezelfde tag de eindstatus overschrijven.

Een generatie-guard maakt van een error-event en een gelijktijdig
hangende keepalive precies een reconnect; de test bootst die race na met
een handmatig te rejecten keepalive-promise en faalt aantoonbaar zodra
de guard weg is. Server-side keepalives voorkomen dat een half-open
verbinding het advisory lock urenlang vasthoudt en niemand meer
verstuurt.

De COPY-regel in de Dockerfile is niet optioneel: zonder die regel zit
instrumentation.ts niet in de image en start de gedeployde server geen
listener.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Alle drie openen met requireWorkersAdmin, met per action een guard-test
die zowel de afwijzing als de afwezigheid van een DB-schrijf assert. Dat
is de regressiebescherming die AC8 niet kan geven, want in workers
bestaan geen non-admin sessies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PUBLIC_PATHS is deny-by-default en de matcher sluit alleen _next en
favicon uit, dus /sw.js moest er expliciet in: anders krijgt een
SW-update na sessie-expiratie een redirect in plaats van een script en
blijft de worker stil op de oude versie hangen. De testmatrix dekt dat
nu af.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
De publieke sleutel komt op request-tijd uit de server component als
prop; geverifieerd tegen de productie-build: de env-naam komt nul keer
voor in de client-bundle en /queue/messages blijft dynamisch.

De Toaster ontbrak volledig in workers, waardoor elke toast in deze app
stil was — inclusief de melding dat de VAPID-sleutel ontbreekt, precies
de feedback die de faalmodus zichtbaar moet maken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs(push): env-vars, kanaal-eis en notificatiesectie
All checks were successful
CI / Verify (pull_request) Successful in 2m28s
38b500ccfc
Documenteert waarom er geen NEXT_PUBLIC_-prefix mag staan, de
nooit-regenereren-zonder-wissen-regel, en welke logregel je moet tellen
bij twijfel over dubbele sends. Het PWA-productdoc is via de MCP
bijgewerkt: dat beweerde nog dat workers geen service worker en geen
push heeft.

Sleutelwaarden staan bewust nergens in git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: COMMENT

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings:

  • minor — lib/push/client.ts:50: unsubscribeFromPush() zegt de browser-subscription eerst lokaal op en roept daarna pas unsubscribeFromPushAction() aan. Als die server action faalt, is de client al uitgeschreven maar blijft de DB-rij bestaan. Dat levert stale subscriptions en misleidende UI/toast-status op; draai de volgorde om of maak de server cleanup expliciet retrybaar/idempotent voordat de browser-state definitief wordt aangepast.

De rest van de diff volgt de admin-only policy (requireWorkersAdmin op actions/page), gebruikt de shared-submodule voor schemawijziging in plaats van lokale migrations, documenteert de VAPID-runtimekeuzes, en bevat gerichte Vitest-dekking voor payload-mapping, server actions, listener-lifecycle en proxy-public path.

## Verdict: COMMENT Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. Findings: - minor — `lib/push/client.ts:50`: `unsubscribeFromPush()` zegt de browser-subscription eerst lokaal op en roept daarna pas `unsubscribeFromPushAction()` aan. Als die server action faalt, is de client al uitgeschreven maar blijft de DB-rij bestaan. Dat levert stale subscriptions en misleidende UI/toast-status op; draai de volgorde om of maak de server cleanup expliciet retrybaar/idempotent voordat de browser-state definitief wordt aangepast. De rest van de diff volgt de admin-only policy (`requireWorkersAdmin` op actions/page), gebruikt de shared-submodule voor schemawijziging in plaats van lokale migrations, documenteert de VAPID-runtimekeuzes, en bevat gerichte Vitest-dekking voor payload-mapping, server actions, listener-lifecycle en proxy-public path.
docs(plans): draaiboek-correcties uit de echte deploy
All checks were successful
CI / Verify (pull_request) Successful in 1m59s
0a5b0ffb07
Twee dingen die de server-uitvoerder terecht opmerkte:

git checkout laat de submodule op de oude commit staan, terwijl de
Dockerfile vendor/scrum4me-shared letterlijk mee de image in kopieert.
Zonder 'git submodule update --init --recursive' bouw je stil de
verkeerde shared-code in, met een groene deploy en werkende UI als
resultaat. Toegevoegd tussen stap 1 en 3.

compose vereist sudo omdat /srv/scrum4me/compose/.env 0600 is, en de
build overschrijft :latest zonder het oude image te bewaren — dus
rollback is een herbouw tenzij je vooraf hertagt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer approved these changes 2026-08-10 17:46:24 +02:00
Dismissed
s4m-codex-reviewer left a comment

Verdict: APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blokkerende of error-severity findings gevonden.

De implementatie volgt de admin-only workers-standaard (requireWorkersAdmin op page/actions), gebruikt de canonical shared-schema route via submodule-bump in plaats van lokale migraties, en voegt passende testdekking toe voor server actions, payload-mapping, listener-lifecycle, push-send gedrag en proxy-public-paths.

# Verdict: APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blokkerende of error-severity findings gevonden. De implementatie volgt de admin-only workers-standaard (`requireWorkersAdmin` op page/actions), gebruikt de canonical shared-schema route via submodule-bump in plaats van lokale migraties, en voegt passende testdekking toe voor server actions, payload-mapping, listener-lifecycle, push-send gedrag en proxy-public-paths.
test(push): tussenverslag AC1, AC3, AC4, AC6 en AC9 groen
All checks were successful
CI / Verify (pull_request) Successful in 2m0s
6886a93692
Server-side bewijs compleet: alle vier de bewijs-ids afgeleverd met
status=201 en verzonden=1/1, alle negatieve controles schoon. De
overgang van verzonden=0/0 naar 1/1 in dezelfde logstroom sluit
'201 op nul ontvangers' uit.

Bevinding: een bericht levert een melding per statuswisseling, niet per
bericht — tot drie per bericht. Dat is ontwerpgedrag ('alle events'),
maar nu meetbaar en dus een keuze die JP moet maken.

De mismatch-hypothese uit de spec is onwaarschijnlijk geworden:
push_subscriptions bevatte nul rijen, dus er is nooit een abonnement
opgeslagen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer approved these changes 2026-08-10 17:56:09 +02:00
Dismissed
s4m-codex-reviewer left a comment

APPROVED

geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen findings. De diff volgt de relevante PWA/productstandaard voor workers: /sw.js is publiek, VAPID wordt runtime gelezen zonder NEXT_PUBLIC_, instrumentation.ts wordt in de Docker-build meegenomen, subscriptions worden met app_scope='workers' gescheiden, en de server actions blijven admin-only. De toegevoegde tests dekken payload-mapping, listener-lifecycle, send/delete-gedrag, proxy-uitzondering en action-auth.
# APPROVED geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen findings. De diff volgt de relevante PWA/productstandaard voor workers: `/sw.js` is publiek, VAPID wordt runtime gelezen zonder `NEXT_PUBLIC_`, `instrumentation.ts` wordt in de Docker-build meegenomen, subscriptions worden met `app_scope='workers'` gescheiden, en de server actions blijven admin-only. De toegevoegde tests dekken payload-mapping, listener-lifecycle, send/delete-gedrag, proxy-uitzondering en action-auth.
fix(push): handel pushsubscriptionchange af — stille doodsoorzaak
All checks were successful
CI / Verify (pull_request) Successful in 2m0s
cb5d0f8858
Waargenomen in productie: het endpoint in de database eindigde op
cZIOLPdS…, terwijl Chrome inmiddels bzSoiimR… gebruikte. De browser had
het abonnement stilletjes vervangen en niets ving dat op.

Dat is niet aan de serverkant te zien: FCM antwoordt op zo'n verouderd
endpoint met 201, want het bestaat nog. Het log meldde verzonden=1/1 met
status=201 terwijl er niets werd afgeleverd — groen log, stille telefoon.
Precies de faalmodus waar dit ontwerp tegen bedoeld is.

De service worker luistert nu naar pushsubscriptionchange, abonneert
zichzelf opnieuw met de sleutel uit het oude abonnement, en meldt het
nieuwe adres via POST /api/push/resubscribe (admin-guarded). Het oude
endpoint wordt daarbij verwijderd, anders blijft het 201's produceren.

De sw.js van de Scrum4Me-hoofdapp mist dezelfde handler; dat gat is
destijds meegeport en is een plausibele verklaring voor de nul rijen die
daar in push_subscriptions stonden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: COMMENT

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • medium — app/api/push/resubscribe/route.ts:63: de cleanup van oldEndpoint filtert alleen op endpoint + app_scope, niet op user_id. Een geauthenticeerde workers-admin kan daardoor via een gemanipuleerde body een workers-pushsubscription van een andere admin verwijderen als het endpoint bekend is. Dit is geen functionele blocker voor aflevering, maar het doorbreekt de ownership-scope die unsubscribeFromPushAction wel hanteert. Voeg user_id: session.userId toe of documenteer expliciet waarom admins elkaars subscriptions mogen opruimen.

Samenvatting

De diff is breed maar coherent: VAPID-config blijft server-side, de public key wordt request-time doorgegeven, /sw.js is publiek bereikbaar voor service-worker updates, de listener gebruikt advisory locking om dubbele sends te vermijden, en de push-payload/server/listener lagen hebben gerichte tests. Er zijn ook docs en een testverslag toegevoegd. Door bovenstaande ownership-opmerking kies ik COMMENT in plaats van APPROVED.

# Verdict: COMMENT Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - medium — `app/api/push/resubscribe/route.ts:63`: de cleanup van `oldEndpoint` filtert alleen op `endpoint` + `app_scope`, niet op `user_id`. Een geauthenticeerde workers-admin kan daardoor via een gemanipuleerde body een workers-pushsubscription van een andere admin verwijderen als het endpoint bekend is. Dit is geen functionele blocker voor aflevering, maar het doorbreekt de ownership-scope die `unsubscribeFromPushAction` wel hanteert. Voeg `user_id: session.userId` toe of documenteer expliciet waarom admins elkaars subscriptions mogen opruimen. ## Samenvatting De diff is breed maar coherent: VAPID-config blijft server-side, de public key wordt request-time doorgegeven, `/sw.js` is publiek bereikbaar voor service-worker updates, de listener gebruikt advisory locking om dubbele sends te vermijden, en de push-payload/server/listener lagen hebben gerichte tests. Er zijn ook docs en een testverslag toegevoegd. Door bovenstaande ownership-opmerking kies ik `COMMENT` in plaats van `APPROVED`.
test(push): AC2 en AC3 groen op beide toestellen + root cause vastgelegd
All checks were successful
CI / Verify (pull_request) Successful in 1m59s
db09484c3c
De 201 was geen bewijs van aflevering: het endpoint in de database was
niet meer dat van Chrome, en FCM antwoordt op zo'n verouderd adres met
201 omdat het nog bestaat. Nul verzendfouten, nul auto-deletes over de
hele levensduur van de container — de opruimlogica vuurt alleen op
404/410 en raakt dit geval dus nooit.

Vastgelegd: het onderzoek en wat er is uitgesloten, de consequentie voor
de bewijsstrategie (n in verzonden=n/m is 'geaccepteerd', niet
'afgeleverd'), de fix, het identieke gat in de Scrum4Me-hoofdapp, en de
timestamp-meetval die mij onderweg een verkeerde conclusie opleverde.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

COMMENT

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • MINORpublic/sw.js:39 / app/api/push/resubscribe/route.ts:27: de pushsubscriptionchange-handler post naar een route die requireWorkersAdmin() vereist. Dat is correct vanuit admin-only/auth-perspectief, maar beperkt precies de herstelcase waarvoor deze handler bedoeld is: subscription-rotatie kan plaatsvinden terwijl er geen geldige workers-sessie meer is, waarna de route 401 geeft en het nieuwe endpoint niet wordt opgeslagen. De reguliere subscribe-flow blijft werken zodra de admin de UI opnieuw opent, dus dit is geen blokkade, maar de code/docs suggereren nu sterker herstel dan de implementatie kan garanderen.

Verdict

Geen blokkerende findings. De hoofdflow houdt zich aan de admin-only productregel (requireWorkersAdmin op page/action/route), gebruikt de gedeelde push_subscriptions-scope expliciet, voegt gerichte tests toe voor actions, payloadmapping, listener en server-send, en documenteert de VAPID/deployvoorwaarden. Door bovenstaande betrouwbaarheidskanttekening kies ik COMMENT in plaats van APPROVED.

# COMMENT Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **MINOR** — `public/sw.js:39` / `app/api/push/resubscribe/route.ts:27`: de `pushsubscriptionchange`-handler post naar een route die `requireWorkersAdmin()` vereist. Dat is correct vanuit admin-only/auth-perspectief, maar beperkt precies de herstelcase waarvoor deze handler bedoeld is: subscription-rotatie kan plaatsvinden terwijl er geen geldige workers-sessie meer is, waarna de route `401` geeft en het nieuwe endpoint niet wordt opgeslagen. De reguliere subscribe-flow blijft werken zodra de admin de UI opnieuw opent, dus dit is geen blokkade, maar de code/docs suggereren nu sterker herstel dan de implementatie kan garanderen. ## Verdict Geen blokkerende findings. De hoofdflow houdt zich aan de admin-only productregel (`requireWorkersAdmin` op page/action/route), gebruikt de gedeelde `push_subscriptions`-scope expliciet, voegt gerichte tests toe voor actions, payloadmapping, listener en server-send, en documenteert de VAPID/deployvoorwaarden. Door bovenstaande betrouwbaarheidskanttekening kies ik `COMMENT` in plaats van `APPROVED`.
feat(push): meldingsfilter — aan jp, alle pending, alle failed
All checks were successful
CI / Verify (pull_request) Successful in 2m2s
b0cb7d4743
Met alles aan gaf de queue tot drie meldingen per bericht (aanmaak,
claim, afronding), ook voor agent-onderling verkeer waar JP niet bij
betrokken is. Keuze van JP na een dag praktijkervaring.

shouldNotify pusht nu bij: bericht aan jp (dekt ook antwoorden op zijn
verzoeken, want die keren de route om), status pending (nieuw werk en
requeues), en status failed. Weg is het agent-onderlinge claimed/done/
cancelled-verkeer plus JP's eigen antwoord.

Overgeslagen envelopes worden gelogd. Zonder die regel is 'geen melding'
niet te onderscheiden van een storing, en dat onderscheid kostte vandaag
een middag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer approved these changes 2026-08-10 19:01:24 +02:00
Dismissed
s4m-codex-reviewer left a comment

APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blokkerende of error-severity findings gevonden.

Reviewnotities

De diff volgt de relevante productstandaarden uit de docs: workers blijft admin-only (requireWorkersAdmin op page/action/route), schemawijzigingen lopen via scrum4me-shared/designated migrator in plaats van eigen workers-migraties, en de Dockerfile neemt instrumentation.ts mee zodat de listener in standalone builds beschikbaar is.

Testdekking is aanwezig voor de belangrijkste risico's: VAPID payload-mapping/filtering, push-send foutclassificatie en secret-safe logging, queue-listener serialisatie/reconnect, server actions, resubscribe-route en proxy-public-path voor /sw.js. Docs zijn bijgewerkt voor configuratie, operationele regels en troubleshooting.

# APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blokkerende of error-severity findings gevonden. ## Reviewnotities De diff volgt de relevante productstandaarden uit de docs: workers blijft admin-only (`requireWorkersAdmin` op page/action/route), schemawijzigingen lopen via `scrum4me-shared`/designated migrator in plaats van eigen workers-migraties, en de Dockerfile neemt `instrumentation.ts` mee zodat de listener in standalone builds beschikbaar is. Testdekking is aanwezig voor de belangrijkste risico's: VAPID payload-mapping/filtering, push-send foutclassificatie en secret-safe logging, queue-listener serialisatie/reconnect, server actions, resubscribe-route en proxy-public-path voor `/sw.js`. Docs zijn bijgewerkt voor configuratie, operationele regels en troubleshooting.
feat(push): log ook waarom er verstuurd is, niet alleen waarom niet
All checks were successful
CI / Verify (pull_request) Successful in 2m1s
b9dd826d1e
De overgeslagen-regel droeg status en bestemming, de verzend-regel niet.
Daardoor was wel na te trekken wat het filter tegenhield, maar niet wat
het doorliet — de verkeerde kant om blind te zijn bij een klacht over
een ongewenste melding. Opgemerkt door de uitvoerder op de server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer requested changes 2026-08-10 19:16:59 +02:00
Dismissed
s4m-codex-reviewer left a comment

Verdict: REQUEST_CHANGES

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • errordocs/reports/2026-08-10-vapid-push-testverslag.md:35: het eigen testverslag markeert AC5 (AC2+AC3+AC4 op iPhone met gesloten PWA) nog als Nog te doen. Voor deze PR is gesloten-PWA/iPhone-aflevering juist een kernclaim van de Web Push-feature; zonder dit bewijs blijft een belangrijk productiepad onverifieerd.
  • errordocs/reports/2026-08-10-vapid-push-testverslag.md:37: AC7 (Twee listeners → precies één push per envelope) staat nog als Nog te doen. De implementatie gebruikt een deployment-breed advisory lock om dubbele sends te voorkomen; zonder expliciete multi-listener-verificatie is een centraal architectuurrisico nog open.
  • errordocs/reports/2026-08-10-vapid-push-testverslag.md:38: AC8 (Uitgelogd → 307, geen rij, geen push) staat nog als Nog te doen. De feature voegt nieuwe subscribe/resubscribe/test-push ingangen toe; auth-/sessiegedrag moet aantoonbaar dicht zijn voordat dit mergebaar is.

Samenvatting

De diff volgt de relevante productstandaarden grotendeels: runtime VAPID zonder NEXT_PUBLIC_, instrumentation.ts wordt in Docker meegenomen, /sw.js is publiek via de proxy, subscriptions zijn gescopeerd met app_scope='workers', en er is gerichte unit-testdekking voor payloadmapping, server-actions, resubscribe, listener-lifecycle en proxy. De PR is alleen niet approvebaar zolang het meegeleverde acceptatie-/testverslag nog expliciet open E2E-criteria bevat.

# Verdict: REQUEST_CHANGES Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **error** — `docs/reports/2026-08-10-vapid-push-testverslag.md:35`: het eigen testverslag markeert AC5 (`AC2+AC3+AC4 op iPhone met gesloten PWA`) nog als **Nog te doen**. Voor deze PR is gesloten-PWA/iPhone-aflevering juist een kernclaim van de Web Push-feature; zonder dit bewijs blijft een belangrijk productiepad onverifieerd. - **error** — `docs/reports/2026-08-10-vapid-push-testverslag.md:37`: AC7 (`Twee listeners → precies één push per envelope`) staat nog als **Nog te doen**. De implementatie gebruikt een deployment-breed advisory lock om dubbele sends te voorkomen; zonder expliciete multi-listener-verificatie is een centraal architectuurrisico nog open. - **error** — `docs/reports/2026-08-10-vapid-push-testverslag.md:38`: AC8 (`Uitgelogd → 307, geen rij, geen push`) staat nog als **Nog te doen**. De feature voegt nieuwe subscribe/resubscribe/test-push ingangen toe; auth-/sessiegedrag moet aantoonbaar dicht zijn voordat dit mergebaar is. ## Samenvatting De diff volgt de relevante productstandaarden grotendeels: runtime VAPID zonder `NEXT_PUBLIC_`, `instrumentation.ts` wordt in Docker meegenomen, `/sw.js` is publiek via de proxy, subscriptions zijn gescopeerd met `app_scope='workers'`, en er is gerichte unit-testdekking voor payloadmapping, server-actions, resubscribe, listener-lifecycle en proxy. De PR is alleen niet approvebaar zolang het meegeleverde acceptatie-/testverslag nog expliciet open E2E-criteria bevat.
test(push): AC7 en AC8 groen
All checks were successful
CI / Verify (pull_request) Successful in 2m4s
aa55cb91ac
AC7: met twee listeners precies een aggregatieregel; de probe meldde
'luistert maar verstuurt niet' en bleef stil. AC8: 307 op beschermde
paden, geen rij bij een POST zonder sessie, en /sw.js geeft 200 zodat
de PUBLIC_PATHS-uitzondering ook in productie bewezen is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blocker/error findings gevonden.

De diff volgt de relevante productstandaarden: de nieuwe page/action/route-paden gebruiken requireWorkersAdmin, er zijn geen lokale workers-migraties toegevoegd maar alleen een shared-submodule bump, en de PR bevat gerichte Vitest-dekking voor server actions, resubscribe-route, payload-mapping, listenergedrag, push-serverfouten en proxy-uitzondering. De Dockerfile neemt instrumentation.ts expliciet mee, wat past bij de Next.js runtime-aanpak voor de queue-listener.

# APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blocker/error findings gevonden. De diff volgt de relevante productstandaarden: de nieuwe page/action/route-paden gebruiken `requireWorkersAdmin`, er zijn geen lokale workers-migraties toegevoegd maar alleen een shared-submodule bump, en de PR bevat gerichte Vitest-dekking voor server actions, resubscribe-route, payload-mapping, listenergedrag, push-serverfouten en proxy-uitzondering. De Dockerfile neemt `instrumentation.ts` expliciet mee, wat past bij de Next.js runtime-aanpak voor de queue-listener.
docs: spec op 'in productie', AC5 expliciet open in het verslag
All checks were successful
CI / Verify (pull_request) Successful in 2m6s
ac11b36f66
Acht van de negen criteria bevestigd. AC5 staat als deels bevestigd: de
aflevering op iPhone is aangetoond, de tag-vervanging op het scherm niet.
Op keuze van JP wordt de PR met dat punt open gemerged in plaats van het
groen te noemen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/scrum4me-workers!81
No description provided.