# Suivi — Notifications Actions (passe 2)

Etat: Itération 1 — en cours (catalogue‑first + snapshot‑light)

## Journal
- [x] Schéma: créer `action_catalog` & `action_catalog_versions`; migrer `notifications_rich` avec `actions_ref_json` (+ `actions_snapshot_json` light)
- [todo] Admin catalogue: CRUD actions/versions, status (draft/active/deprecated/disabled), calcul `etag`
- [x] Admin catalogue: CRUD actions/versions, status (draft/active/deprecated/disabled), calcul `etag` (backend)
- [todo] Admin notifs: sélectionner depuis le catalogue `{ref,version}` + `params`; persister `actions_ref_json`; matérialiser `actions_snapshot_json`
- [x] Lecture: `ActionResolver` (cache mémoire) pour résoudre refs → manifeste (vérifier `versionEtag` du snapshot); exposer `actions`, `manifestVersion`, `etag`
- [todo] Index & contraintes: `(action_id, status)`, `UNIQUE(action_id,version,etag)` optionnelle; politique mismatch snapshot (policy=snapshot), `ACTION_DISABLED(403)`
- [todo] Canonical ETag: concat `versionEtag` triés + JSON(params) canonique (clés triées) → sha256
- [x] Validation Admin stricte: forcer `additionalProperties:false` si absent, squelettes guidés côté UI; whitelist `kind`/`capabilities`
- [x] Iframe: sanitize + expansion `[sb:action]` → `<button data-sb-action-id>`; supprimer `content_js`; CSP avec nonce
- [x] Parent: registre frames+nonces; routeur `message`; `dispatchAction()` → `/api/commands`; intégration orchestrateur `OpenDialog`
- [x] Parent: mapping 409 idempotent → succès + toasts minimaux
- [todo] UI: états pending/success/error; désactivation bouton; toasts erreurs canoniques
- [todo] Audit: correlationId propagé + auditTag dérivé serveur

## Checklist (DoD Itération 1)
- [ ] Sanitize HTML notif (scripts/`on*` supprimés; whitelist tags/attrs)
- [ ] Shortcodes `[sb:action]` → boutons avec `aria-label`
- [x] Iframe sandbox + CSP: `connect-src 'none'`, `script-src 'nonce-…'`
- [ ] Parent: rejet messages hors frames/nonce; handshake/cleanup OK
- [ ] Actions low‑risk: Subscribe/Unsubscribe/Override via `/api/commands`
- [ ] Catalogue Admin: création/versionnage, statut, `etag` calculé
- [ ] Notifs Admin: références + snapshot‑light (labels finalisés, params, `versionEtag`)
- [x] Resolver: cache clé `action:{action_id}:v{version}`; invalidation au bump; `etag` combiné canonicalisé
- [ ] Codes: inclure `ACTION_DISABLED(403)`, `RATE_LIMITED(429)` dans le mapping UI
- [x] Parent UX: bouton `disabled` + `aria-busy="true"`; 409 traité en succès idempotent
- [ ] Erreurs canoniques (401/403/422/409) et mapping UI
- [ ] Idempotence vérifiée (double‑clic)
- [ ] Docs à jour (README/PLAN/PROGRESS)

## Phasage — tâches
- I1 — Catalogue‑first (subscribe/unsubscribe/override) + palette admin (catalogue) + snapshot‑light + durcissement CSP/sanitize
- I2 — ActionToken JIT (HMAC/TTL, lié à manifestVersion) + capabilities renforcées + UX palette
- I3 — Catalogue élargi (trophées, ban, préférences self), télémétrie agrégée

## Notes techniques
- MySQL: dédup non‑idempotent via table `action_dedup` UNIQUE (user_id, notification_id, action_id, params_hash, window_bucket); `INSERT IGNORE`
- Rate‑limit par (userId, actionId) via table dédiée (analogue `ip_rec`) — seuil 5/min (burst 3) si besoin
- postMessage: `origin=null` en `srcdoc`; sécurité fondée sur `(event.source, nonce)`
- `actionToken` (I2): HMAC(uid, notifId, action_id, version, paramsHash, exp, etagVersion) — TTL court
