metallkart-erp/docs/diagnostics/SPEC-1F-PUSH-DIAGNOSTIC-RAPPORT.md
louis 47938b6d57 docs(1f): diagnostic push subscription validation
Scenario A confirme: pipeline push complet fonctionne.
Le fallback ERP_IN_APP etait cause par l'absence de channel: 'PWA_PUSH'
dans le test-dispatch. La subscription WNS de Louis existe en DB.
Commande de validation incluse dans le rapport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-28 22:42:29 +04:00

9.4 KiB

SPEC-1F-PUSH-DIAGNOSTIC-RAPPORT

Date : 2026-05-28 Auteur : Claude Code (Session 147) Statut : Diagnostic complet, Scenario A confirme


1. Resume executif

Le pipeline push fonctionne de bout en bout. La subscription WNS de Louis existe en DB (push_subscriptions, 1 row, failure_count=0). Le fallback vers ERP_IN_APP lors du test etait un faux negatif : le endpoint POST /admin/test-dispatch sans le champ channel: 'PWA_PUSH' laisse le router choisir le premier canal disponible dans ESCALATION_ORDER, et ERP_IN_APP est toujours en tete. Il suffit de refaire le test avec channel: 'PWA_PUSH' explicite pour declencher un vrai push WNS.


2. Findings par sous-jalon

S1 — Route REST d'enregistrement push

Fichier : src/modules/notification-router/notification-router.routes.ts (L101-157) Prefix : /api/v1/notification-router (enregistre dans server.ts L187)

Methode URL Auth Description
POST /api/v1/notification-router/push-subscriptions Oui Upsert subscription
DELETE /api/v1/notification-router/push-subscriptions Oui Remove by endpoint
GET /api/v1/notification-router/push-subscriptions Oui List user devices

Schema Zod (L12-19) :

const pushSubscriptionSchema = z.object({
  endpoint: z.string().url(),
  keys: z.object({
    p256dh: z.string().min(1),
    auth: z.string().min(1),
  }),
  userAgent: z.string().optional(),
});

Note : La tentative de Louis via POST /api/v1/push-subscriptions renvoyait 404 car le prefix correct est /api/v1/notification-router/push-subscriptions.

S2 — Cablage frontend

Fichiers cles :

  • frontend/src/services/push-subscription.service.ts — service complet (subscribe/unsubscribe)
  • frontend/src/hooks/usePushSubscription.ts — React hook
  • frontend/src/components/NotificationOptInBanner.tsx — banner opt-in
  • frontend/src/pages/settings/NotificationsTab.tsx — section PushSubscriptions

Verification URL matching :

Operation Backend Frontend (client.post(...)) Match
Register POST /api/v1/notification-router/push-subscriptions client.post('/notification-router/push-subscriptions', {...}) OK
Delete DELETE /api/v1/notification-router/push-subscriptions client.delete('/notification-router/push-subscriptions', {...}) OK
List GET /api/v1/notification-router/push-subscriptions client.get('/notification-router/push-subscriptions') OK

Flux frontend :

  1. Banner affiche "Включить уведомления" si isSupported && !isSubscribed && permission !== 'denied'
  2. Click "Включить" -> Notification.requestPermission() -> pushManager.subscribe({ applicationServerKey: VITE_VAPID_PUBLIC_KEY })
  3. POST backend avec { endpoint, keys: { p256dh, auth }, userAgent }
  4. DB upsert (unique: userId + endpoint)

Le frontend est correctement cable. L'enregistrement a effectivement fonctionne (voir S3).

S3 — Etat DB push_subscriptions

 id                                   | userId                               | endpoint (tronque)
--------------------------------------+--------------------------------------+---------------------------------------------------
 c2aa4a25-73b5-4272-8fc6-be587b18e36c | ebdd4d55-9aeb-45c3-a0b4-577dafbab887 | https://wns2-db5p.notify.windows.com/w/?token=BQY...
Metrique Valeur
Total rows 1
userId Louis ebdd4d55-9aeb-45c3-a0b4-577dafbab887
Endpoint WNS Microsoft (Edge/Windows)
User-Agent Chrome/148 Edge/148 Windows 10
failure_count 0
created_at 2026-05-28 17:44:56 UTC
last_used_at NULL (jamais utilise pour un push reel)

Conclusion S3 : La subscription existe, est active, et n'a jamais ete utilisee pour envoyer un push.

S4 — Config VAPID backend

Variable Present Coherent
VAPID_PUBLIC_KEY Oui (BMXad-r94...) -
VAPID_PRIVATE_KEY Oui (REDACTED) -
VITE_VAPID_PUBLIC_KEY Oui (BMXad-r94...) Identique a VAPID_PUBLIC_KEY
VAPID_SUBJECT Non explicite Default mailto:erp_bot@metdesigntver.ru dans le code

Initialisation : ensureVapid() est lazy (L23-35 de pwa-push.adapter.ts) — appelee au premier send(). Puisque aucun push n'a ete envoye, VAPID n'a pas ete initialise. Ce n'est PAS un bug — c'est by design.

S5 — Logs backend

Le backend a ete restart a 05:35 MSK (soak T0). Les logs /tmp/erp-backend.log ne contiennent aucune trace push/delivery car :

  • Aucun push n'a ete tente depuis le restart
  • Les 2 deliveries (943e865b et 29fd6620) datent d'AVANT le restart (18:07 et 18:38 UTC = session precedente)

Deliveries en DB :

deliveryId channel status createdAt
943e865b-ebcb-4f77-bb16-9d62ba0dd10f ERP_IN_APP SENT 18:07:56 UTC
29fd6620-05f5-4c69-8a90-f6cf9426ceca ERP_IN_APP SENT 18:38:55 UTC

Les deux sont ERP_IN_APP — pas de tentative PWA_PUSH. Raison : voir S6.

S6 — Diagnostic : Scenario A confirme

Cause racine : Le POST /admin/test-dispatch de Louis n'a PAS specifie channel: 'PWA_PUSH'.

Mecanisme :

  1. testDispatchSchema (L29-35) : le champ channel est optionnel (.optional())
  2. router.dispatch({ preferredChannel: data.channel }) — si channel absent, preferredChannel = undefined
  3. resolveChannel() (L52-69 de router.service.ts) : sans preferredChannel, itere ESCALATION_ORDER:
    ESCALATION_ORDER = ['ERP_IN_APP', 'PWA_PUSH', 'TELEGRAM', 'EMAIL']
    
  4. ERP_IN_APP est TOUJOURS available (L60: case 'ERP_IN_APP': return true)
  5. Le router s'arrete au premier canal disponible avec adapter enregistre → ERP_IN_APP

Ce n'est PAS un bug. Le router fait exactement ce qu'il est concu pour faire. Le test n'utilisait pas le bon parametre.

S7 — Commande de validation pour Louis

Commande curl (depuis le VPS) :

# 1. Login pour obtenir un token
TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"gruart@cifem-rus.ru","password":"MetallKart2026!"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))")

# 2. Test dispatch avec channel PWA_PUSH explicite
curl -X POST http://localhost:3001/api/v1/notification-router/admin/test-dispatch \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "targetUserId": "ebdd4d55-9aeb-45c3-a0b4-577dafbab887",
    "channel": "PWA_PUSH",
    "title": "Test Push 1F - Validation",
    "body": "Si vous voyez cette notification Windows, le push fonctionne !",
    "priority": "HIGH"
  }'

Resultat attendu :

  • channelUsed: "PWA_PUSH", status: "SENT"
  • Notification OS Windows apparait sur le PC de Louis (Edge/Chrome doit etre ouvert ou le SW actif)
  • last_used_at mis a jour dans push_subscriptions

Alternative — commande fetch console navigateur : Si Louis prefere tester depuis son navigateur (console DevTools sur https://erp-staging.metdesigntver.ru/) :

(async () => {
  const r = await fetch('/api/v1/notification-router/admin/test-dispatch', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      targetUserId: 'ebdd4d55-9aeb-45c3-a0b4-577dafbab887',
      channel: 'PWA_PUSH',
      title: 'Test Push 1F - Console',
      body: 'Push depuis la console navigateur',
      priority: 'HIGH'
    })
  });
  console.log('STATUS:', r.status, '| BODY:', await r.json());
})();

3. Scenario identifie

Scenario A — Le pipeline complet fonctionne. Le test initial n'utilisait pas le parametre channel: 'PWA_PUSH'.

  • La route d'enregistrement existe et est correctement cablee
  • Le frontend l'appelle correctement (preuve : 1 row en DB)
  • Le PwaPushAdapter est enregistre dans le router
  • Les VAPID keys sont coherentes back/front
  • Il suffit de refaire le test avec channel: 'PWA_PUSH' explicite

4. Dette technique decouverte

Item Severite Description
DT-1 P3 VAPID_SUBJECT non explicite dans .env VPS (fallback code OK mais fragile)
DT-2 P4 last_used_at jamais mis a jour (reste NULL apres le premier push) — a verifier dans PwaPushAdapter.send()
DT-3 P4 Le test-dispatch sans channel explicite est trompeur — ajouter un champ channelUsed dans le message de reponse ou un warning si channel absent
DT-4 Info La 404 de Louis sur /api/v1/push-subscriptions venait d'un mauvais path — la doc utilisateur pourrait lister les endpoints disponibles

5. Fichiers de reference

Fichier Role
src/modules/notification-router/notification-router.routes.ts Routes push (L101-157) + test-dispatch (L208-229)
src/modules/notification-router/router.service.ts resolveChannel (L52-69)
src/modules/notification-router/user-context.service.ts hasPushSubscription (L31, L48, L63)
src/modules/notification-router/adapters/pwa-push.adapter.ts PwaPushAdapter + ensureVapid lazy
src/modules/notification-router/router.constants.ts ESCALATION_ORDER
frontend/src/services/push-subscription.service.ts Frontend push service
frontend/src/hooks/usePushSubscription.ts React hook
frontend/src/components/NotificationOptInBanner.tsx Banner opt-in