Profile deletion (and every confirm) proceeds without waiting for the confirmation #421

Closed
opened 2026-10-12 20:04:40 +00:00 by maximus · 2 comments
Owner

Contexte

Dans Paramètres → Profils, supprimer un profil secondaire efface le profil sans attendre la réponse à la boîte de confirmation : la boîte s'affiche alors que la suppression est déjà partie, et « Annuler » n'empêche rien. Un refus se comporte comme une confirmation. La base du profil est supprimée du disque par delete_profile_db : perte de données irréversible. Signalé par Max le 2026-10-12.

Cause (vérifiée dans le code, main 1954502)

tauri-plugin-dialog 2.6.0 (enregistré dans src-tauri/src/lib.rs:391) injecte sur desktop un script d'initialisation (init-iife.js, js_init_script hors Android) qui remplace window.confirm par une fonction async :

window.confirm = async function (i) { return await invoke("plugin:dialog|confirm", { message: i.toString() }) }

confirm(...) renvoie donc une Promise, toujours vraie. Le garde de ProfileFormModal.tsx:91 ne bloque jamais :

if (!confirm(t("profile.deleteConfirm"))) return;   // !Promise === false
await deleteProfile(id);                            // runs immediately

TypeScript ne voit rien : le type DOM de window.confirm reste boolean. Le plugin est présent depuis 49e0bd2 (2026-02-08), la suppression de profil depuis la v0.3.0 (20cae64) : le défaut est dans toutes les versions publiées.

Portée : toutes les confirmations de l'app

Site Action exécutée sans confirmation
src/components/profile/ProfileFormModal.tsx:91 suppression d'un profil et de sa base
src/components/categories/CategoryDetailPanel.tsx:44 suppression d'une catégorie
src/components/adjustments/AdjustmentDetailPanel.tsx:39 suppression d'un ajustement
src/components/budget/TemplateActions.tsx:61 suppression d'un gabarit de budget
src/pages/AccountsPage.tsx:126 suppression d'une catégorie du Bilan
src/components/settings/LicenseCard.tsx:161 → src/hooks/useLicenseMachines.ts:383 et :391 retrait de la clé de licence, y compris « retirer sans libérer la place »

Les tests de performRemoveLicense (useLicenseMachines.test.ts:752) injectent un confirm synchrone : le vert ne couvre pas le chemin réel.

Décisions (Max, 2026-10-12)

  • Approche : un helper unique confirmAction() qui appelle ask() de @tauri-apps/plugin-dialog, boutons libellés FR/EN, kind: "warning". Les sites font await. Écartés : await window.confirm (boutons natifs non traduits, type boolean mensonger) et une modale React maison (plus de code pour le même résultat).
  • Portée : les 6 sites dans cette issue, une seule PR.

Travail à faire

  • Ajouter @tauri-apps/plugin-dialog au package.json (borne ^2.6.0, alignée sur le crate du lock). La permission dialog:default de src-tauri/capabilities/default.json inclut déjà allow-ask : rien à changer côté Rust.
  • Créer le helper (ex. src/services/confirmService.ts) : confirmAction(message: string, opts?): Promise<boolean> qui appelle ask(message, { kind: "warning", okLabel: t("common.confirm…"), cancelLabel: t("common.cancel…") }). Réutiliser des clés i18n existantes si elles existent, sinon en ajouter dans fr.json et en.json. Un échec de l'appel (ask qui rejette) vaut refus (fail closed) et passe par logWarn.
  • Remplacer les 6 sites par if (!(await confirmAction(...))) return;.
  • useLicenseMachines.ts : passer le type de deps.confirm à (message: string) => Promise<boolean>, await aux lignes 383 et 391 ; LicenseCard.tsx:161 passe confirmAction. Adapter les tests pour un confirm asynchrone.
  • Garde contre la récidive : un test vitest qui parcourt src/**/*.{ts,tsx} (hors tests) et échoue sur tout appel à confirm( / window.confirm( / alert( hors du helper (pas d'ESLint dans le projet). alert est aussi remplacé par le plugin (non bloquant, sans valeur de retour) : le garde l'interdit pour éviter qu'un futur code compte sur son blocage.
  • CHANGELOG.md / CHANGELOG.fr.md, section Fixed/Corrigé sous [Unreleased] : les suppressions (profil, catégorie, ajustement, gabarit, catégorie du Bilan) et le retrait de la clé attendent la confirmation.
  • docs/architecture.md : mentionner le helper dans la liste des services.

Hors portée, à noter

  • prompt(...) de ProfileFormModal.tsx:102 (saisie du PIN) n'est pas remplacé par le plugin. Son comportement dans WebKitGTK (Linux) et WebView2 (Windows) n'est pas vérifié ; à observer pendant la vérification manuelle, issue séparée si la saisie échoue.

Fichiers concernés

  • package.json, package-lock.json — paquet JS du plugin dialog
  • src/services/confirmService.ts (nouveau) — helper
  • src/components/profile/ProfileFormModal.tsx, src/components/categories/CategoryDetailPanel.tsx, src/components/adjustments/AdjustmentDetailPanel.tsx, src/components/budget/TemplateActions.tsx, src/pages/AccountsPage.tsx — await confirmAction
  • src/components/settings/LicenseCard.tsx, src/hooks/useLicenseMachines.ts — confirmation asynchrone dans performRemoveLicense
  • src/i18n/locales/fr.json, en.json — libellés des boutons si absents
  • CHANGELOG.md, CHANGELOG.fr.md, docs/architecture.md

Surface de test

  • Existants : src/hooks/useLicenseMachines.test.ts (confirm synchrone, à passer en asynchrone).
  • Manquants :
    • confirmService : ask résolu true → true, false → false, rejet → false + logWarn (mock de @tauri-apps/plugin-dialog).
    • performRemoveLicense : confirmation résolue à false → aucun removeLicense appelé ; idem pour la seconde confirmation (ligne 391). Le test doit rougir si l'await est retiré (une Promise est vraie).
    • Test de garde du code source décrit plus haut.
  • Régression requise : oui. Mutations à passer : retirer l'await d'un site, remplacer confirmAction par window.confirm, rendre le rejet de ask « vrai ».

Critères d'acceptation

  • Sur les 6 sites, « Annuler » (ou la fermeture de la boîte) laisse les données intactes ; rien ne part avant « OK ».
  • Les boutons de la boîte sont en français ou en anglais selon la langue de l'app.
  • Un échec du dialogue vaut refus.
  • Aucun appel à confirm/alert natif ne reste dans src/ hors du helper, et un test le garantit.
  • npm run build et npm test verts ; CHANGELOG FR/EN à jour.
  • Vérification manuelle sous Linux (et Windows si possible) : supprimer un profil secondaire puis Annuler → profil et base présents ; OK → supprimés.

Complexité estimée

Simple à moyenne : 6 sites mécaniques, un helper, un changement de signature dans performRemoveLicense et ses tests, un test de garde.

## Contexte Dans Paramètres → Profils, supprimer un profil secondaire efface le profil **sans attendre la réponse** à la boîte de confirmation : la boîte s'affiche alors que la suppression est déjà partie, et « Annuler » n'empêche rien. Un refus se comporte comme une confirmation. La base du profil est supprimée du disque par `delete_profile_db` : perte de données irréversible. Signalé par Max le 2026-10-12. ### Cause (vérifiée dans le code, `main` `1954502`) `tauri-plugin-dialog` 2.6.0 (enregistré dans `src-tauri/src/lib.rs:391`) injecte sur desktop un script d'initialisation (`init-iife.js`, `js_init_script` hors Android) qui **remplace `window.confirm` par une fonction `async`** : ```js window.confirm = async function (i) { return await invoke("plugin:dialog|confirm", { message: i.toString() }) } ``` `confirm(...)` renvoie donc une `Promise`, toujours vraie. Le garde de `ProfileFormModal.tsx:91` ne bloque jamais : ```ts if (!confirm(t("profile.deleteConfirm"))) return; // !Promise === false await deleteProfile(id); // runs immediately ``` TypeScript ne voit rien : le type DOM de `window.confirm` reste `boolean`. Le plugin est présent depuis `49e0bd2` (2026-02-08), la suppression de profil depuis la v0.3.0 (`20cae64`) : le défaut est dans toutes les versions publiées. ### Portée : toutes les confirmations de l'app | Site | Action exécutée sans confirmation | |---|---| | `src/components/profile/ProfileFormModal.tsx:91` | suppression d'un profil et de sa base | | `src/components/categories/CategoryDetailPanel.tsx:44` | suppression d'une catégorie | | `src/components/adjustments/AdjustmentDetailPanel.tsx:39` | suppression d'un ajustement | | `src/components/budget/TemplateActions.tsx:61` | suppression d'un gabarit de budget | | `src/pages/AccountsPage.tsx:126` | suppression d'une catégorie du Bilan | | `src/components/settings/LicenseCard.tsx:161` → `src/hooks/useLicenseMachines.ts:383` et `:391` | retrait de la clé de licence, y compris « retirer sans libérer la place » | Les tests de `performRemoveLicense` (`useLicenseMachines.test.ts:752`) injectent un `confirm` synchrone : le vert ne couvre pas le chemin réel. ## Décisions (Max, 2026-10-12) - **Approche** : un helper unique `confirmAction()` qui appelle `ask()` de `@tauri-apps/plugin-dialog`, boutons libellés FR/EN, `kind: "warning"`. Les sites font `await`. Écartés : `await window.confirm` (boutons natifs non traduits, type `boolean` mensonger) et une modale React maison (plus de code pour le même résultat). - **Portée** : les 6 sites dans cette issue, une seule PR. ## Travail à faire - [ ] Ajouter `@tauri-apps/plugin-dialog` au `package.json` (borne `^2.6.0`, alignée sur le crate du lock). La permission `dialog:default` de `src-tauri/capabilities/default.json` inclut déjà `allow-ask` : rien à changer côté Rust. - [ ] Créer le helper (ex. `src/services/confirmService.ts`) : `confirmAction(message: string, opts?): Promise<boolean>` qui appelle `ask(message, { kind: "warning", okLabel: t("common.confirm…"), cancelLabel: t("common.cancel…") })`. Réutiliser des clés i18n existantes si elles existent, sinon en ajouter dans `fr.json` et `en.json`. Un échec de l'appel (`ask` qui rejette) vaut **refus** (fail closed) et passe par `logWarn`. - [ ] Remplacer les 6 sites par `if (!(await confirmAction(...))) return;`. - [ ] `useLicenseMachines.ts` : passer le type de `deps.confirm` à `(message: string) => Promise<boolean>`, `await` aux lignes 383 et 391 ; `LicenseCard.tsx:161` passe `confirmAction`. Adapter les tests pour un `confirm` asynchrone. - [ ] Garde contre la récidive : un test vitest qui parcourt `src/**/*.{ts,tsx}` (hors tests) et échoue sur tout appel à `confirm(` / `window.confirm(` / `alert(` hors du helper (pas d'ESLint dans le projet). `alert` est aussi remplacé par le plugin (non bloquant, sans valeur de retour) : le garde l'interdit pour éviter qu'un futur code compte sur son blocage. - [ ] CHANGELOG.md / CHANGELOG.fr.md, section Fixed/Corrigé sous `[Unreleased]` : les suppressions (profil, catégorie, ajustement, gabarit, catégorie du Bilan) et le retrait de la clé attendent la confirmation. - [ ] `docs/architecture.md` : mentionner le helper dans la liste des services. ## Hors portée, à noter - `prompt(...)` de `ProfileFormModal.tsx:102` (saisie du PIN) n'est pas remplacé par le plugin. Son comportement dans WebKitGTK (Linux) et WebView2 (Windows) n'est pas vérifié ; à observer pendant la vérification manuelle, issue séparée si la saisie échoue. ## Fichiers concernés - `package.json`, `package-lock.json` — paquet JS du plugin dialog - `src/services/confirmService.ts` (nouveau) — helper - `src/components/profile/ProfileFormModal.tsx`, `src/components/categories/CategoryDetailPanel.tsx`, `src/components/adjustments/AdjustmentDetailPanel.tsx`, `src/components/budget/TemplateActions.tsx`, `src/pages/AccountsPage.tsx` — `await confirmAction` - `src/components/settings/LicenseCard.tsx`, `src/hooks/useLicenseMachines.ts` — confirmation asynchrone dans `performRemoveLicense` - `src/i18n/locales/fr.json`, `en.json` — libellés des boutons si absents - `CHANGELOG.md`, `CHANGELOG.fr.md`, `docs/architecture.md` ## Surface de test - Existants : `src/hooks/useLicenseMachines.test.ts` (confirm synchrone, à passer en asynchrone). - Manquants : - `confirmService` : `ask` résolu `true` → `true`, `false` → `false`, rejet → `false` + `logWarn` (mock de `@tauri-apps/plugin-dialog`). - `performRemoveLicense` : confirmation résolue à `false` → aucun `removeLicense` appelé ; idem pour la seconde confirmation (ligne 391). Le test doit rougir si l'`await` est retiré (une `Promise` est vraie). - Test de garde du code source décrit plus haut. - Régression requise : oui. Mutations à passer : retirer l'`await` d'un site, remplacer `confirmAction` par `window.confirm`, rendre le rejet de `ask` « vrai ». ## Critères d'acceptation - [ ] Sur les 6 sites, « Annuler » (ou la fermeture de la boîte) laisse les données intactes ; rien ne part avant « OK ». - [ ] Les boutons de la boîte sont en français ou en anglais selon la langue de l'app. - [ ] Un échec du dialogue vaut refus. - [ ] Aucun appel à `confirm`/`alert` natif ne reste dans `src/` hors du helper, et un test le garantit. - [ ] `npm run build` et `npm test` verts ; CHANGELOG FR/EN à jour. - [ ] Vérification manuelle sous Linux (et Windows si possible) : supprimer un profil secondaire puis Annuler → profil et base présents ; OK → supprimés. ## Complexité estimée Simple à moyenne : 6 sites mécaniques, un helper, un changement de signature dans `performRemoveLicense` et ses tests, un test de garde.
maximus added the
status:triage
type:bug
source:human
labels 2026-10-12 20:04:40 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-10-12 20:08:35 +00:00
maximus added
status:review
and removed
status:ready
labels 2026-10-18 17:48:05 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-18 18:16:33 +00:00
maximus added
status:blocked
and removed
status:approved
labels 2026-10-18 18:32:08 +00:00
Author
Owner

Correctif mergé par la PR #465 (avance rapide par l'API, main ff9656d, check-frontend vert, 2297 tests ; revue APPROVE de niveau haut au commentaire 24953, suggestion 1 appliquée en tête, commentaire 25025). Suggestion 2 en suivi : #467.

L'issue reste ouverte (status:blocked) : elle attend la vérification manuelle de Max, sur un build de main ou sur la prochaine release.

  1. Sous Linux, Paramètres → Profils : supprimer un profil secondaire puis Annuler → profil et fichier .db (sous app_config_dir()) toujours là ; recommencer puis Confirmer → supprimés. Boutons « Confirmer » / « Annuler » en français, « Confirm » / « Cancel » en anglais. Observer au passage le prompt() du PIN (issue séparée si la saisie échoue).
  2. Même contrôle sur un autre site (catégorie ou gabarit de budget), et sous Windows si possible.
  3. Fermeture à la main une fois 1 et 2 faits, avec les preuves.
Correctif mergé par la PR #465 (avance rapide par l'API, `main` `ff9656d`, `check-frontend` vert, 2297 tests ; revue APPROVE de niveau haut au commentaire 24953, suggestion 1 appliquée en tête, commentaire 25025). Suggestion 2 en suivi : #467. L'issue reste ouverte (`status:blocked`) : elle attend la vérification manuelle de Max, sur un build de `main` ou sur la prochaine release. 1. Sous Linux, Paramètres → Profils : supprimer un profil secondaire puis Annuler → profil et fichier `.db` (sous `app_config_dir()`) toujours là ; recommencer puis Confirmer → supprimés. Boutons « Confirmer » / « Annuler » en français, « Confirm » / « Cancel » en anglais. Observer au passage le `prompt()` du PIN (issue séparée si la saisie échoue). 2. Même contrôle sur un autre site (catégorie ou gabarit de budget), et sous Windows si possible. 3. Fermeture à la main une fois 1 et 2 faits, avec les preuves.
maximus referenced this issue from a commit 2026-10-18 18:33:47 +00:00
Author
Owner

Vérification manuelle faite par Max le 2026-10-18 sur la v0.17.2 publiée, sous Linux, installée par la mise à jour automatique depuis la v0.17.1 : concluante. Windows non déroulé ; le détail des sites essayés n'est pas consigné.

Correctif publié dans la v0.17.2 (PR #465, ff9656d). Fermeture à la main sur décision de Max, la PR portant Refs #421. Reste en suivi : #467 (tests de comportement des quatre sites tenus par la seule garde).

Vérification manuelle faite par Max le 2026-10-18 sur la v0.17.2 publiée, sous Linux, installée par la mise à jour automatique depuis la v0.17.1 : concluante. Windows non déroulé ; le détail des sites essayés n'est pas consigné. Correctif publié dans la v0.17.2 (PR #465, `ff9656d`). Fermeture à la main sur décision de Max, la PR portant `Refs #421`. Reste en suivi : #467 (tests de comportement des quatre sites tenus par la seule garde).
maximus removed the
status:blocked
label 2026-10-18 20:50:34 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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: maximus/Simpl-Resultat#421
No description provided.