From 80c28d43ac3d838233d116e799af7449b0f97f21 Mon Sep 17 00:00:00 2001 From: le king fu Date: Mon, 27 Apr 2026 08:41:15 -0400 Subject: [PATCH 1/2] feat(prices): Settings revocation toggle for price_fetching_consent - Adds PriceFetchConsentToggle to SettingsPage Privacy section - Reads/writes user_preferences.price_fetching_consent for active profile - Confirmation dialog before revoke (DELETE the key entirely so next click re-opens consent modal) - Disabled (with notPremium tooltip) when license is not premium - Adds deletePreference() to userPreferenceService - Adds settings.privacy.title i18n key (FR + EN) - 10 vitest tests covering all paths Closes #159 --- decisions-log.md | 19 ++ .../settings/PriceFetchConsentToggle.test.tsx | 212 ++++++++++++++++++ .../settings/PriceFetchConsentToggle.tsx | 173 ++++++++++++++ src/i18n/locales/en.json | 1 + src/i18n/locales/fr.json | 1 + src/pages/SettingsPage.tsx | 4 + src/services/userPreferenceService.ts | 8 + 7 files changed, 418 insertions(+) create mode 100644 src/components/settings/PriceFetchConsentToggle.test.tsx create mode 100644 src/components/settings/PriceFetchConsentToggle.tsx diff --git a/decisions-log.md b/decisions-log.md index f8bbfd5..293311b 100644 --- a/decisions-log.md +++ b/decisions-log.md @@ -42,6 +42,25 @@ hardcoded fallback in SnapshotEditor wiring. A `// TODO: asset_type from categor comment marks the injection point. A follow-up issue should add `asset_type TEXT DEFAULT 'stock'` to `balance_categories` and thread it through SnapshotEditor props. +## Issue #159 — deletePreference added to userPreferenceService (LOW) + +The `userPreferenceService` had no `deletePreference` function. Added a simple DELETE +SQL helper alongside `getPreference`/`setPreference`. Revoke must DELETE the row entirely +(not set value to null) so that `PriceFetchControl` re-shows the consent modal on next click +(it checks `getPreference` returning a truthy value). Setting to null would break that check. + +## Issue #159 — no Switch/Toggle component in shared/ (MEDIUM) + +No reusable Switch or Toggle component exists in `src/components/shared/`. A minimal +toggle was built inline inside `PriceFetchConsentToggle` using a styled ` + + + {/* Confirmation dialog for revoke */} + {showConfirm && ( +
+

+ {t("settings.privacy.priceFetchConsent.confirmRevoke")} +

+
+ + +
+
+ )} + + ); +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e4851ea..0a6f2e6 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -637,6 +637,7 @@ ] }, "privacy": { + "title": "Privacy", "priceFetchConsent": { "label": "Price fetching via Maximus", "description": "Allow Simpl'Résultat to use the Maximus proxy to fetch asset prices. Privacy: your IP is hidden.", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 78937c2..2e3d121 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -637,6 +637,7 @@ ] }, "privacy": { + "title": "Confidentialité", "priceFetchConsent": { "label": "Récupération de prix via Maximus", "description": "Permet à Simpl'Résultat d'utiliser le proxy Maximus pour récupérer les prix d'actifs. Privacy : ton IP est masquée.", diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 7d3c43a..2507cd7 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -24,6 +24,7 @@ import AccountCard from "../components/settings/AccountCard"; import LogViewerCard from "../components/settings/LogViewerCard"; import TokenStoreFallbackBanner from "../components/settings/TokenStoreFallbackBanner"; import CategoriesCard from "../components/settings/CategoriesCard"; +import { PriceFetchConsentToggle } from "../components/settings/PriceFetchConsentToggle"; export default function SettingsPage() { const { t, i18n } = useTranslation(); @@ -302,6 +303,9 @@ export default function SettingsPage() { {/* Data management */} + {/* Privacy — price fetching consent (premium only) */} + + {/* Data safety notice */}
diff --git a/src/services/userPreferenceService.ts b/src/services/userPreferenceService.ts index a7b49b5..f8bf7bf 100644 --- a/src/services/userPreferenceService.ts +++ b/src/services/userPreferenceService.ts @@ -23,6 +23,14 @@ export async function setPreference( ); } +export async function deletePreference(key: string): Promise { + const db = await getDb(); + await db.execute( + "DELETE FROM user_preferences WHERE key = $1", + [key] + ); +} + export async function getImportFolder(): Promise { return getPreference("import_folder"); } From a6097afcf3f94cdc5e736b08cd5e706991960f6f Mon Sep 17 00:00:00 2001 From: le king fu Date: Mon, 27 Apr 2026 21:35:57 -0400 Subject: [PATCH 2/2] chore: drop decisions-log.md (autopilot scratch, conflicts with main cleanup) --- decisions-log.md | 70 ------------------------------------------------ 1 file changed, 70 deletions(-) delete mode 100644 decisions-log.md diff --git a/decisions-log.md b/decisions-log.md deleted file mode 100644 index 293311b..0000000 --- a/decisions-log.md +++ /dev/null @@ -1,70 +0,0 @@ -# Decisions Log — /autopilot run of 2026-04-27 - -## Issue #156 — session cap budget policy (MEDIUM) - -The 100-request session cap is checked BEFORE rate-limit enforcement and in-flight -deduplication. Successful fetches increment the counter; failures (4xx, 5xx, network) -do NOT consume the budget. Rationale: a user who hits a bad symbol or an auth error -should not have their session budget drained by error conditions outside their control. -This is the most user-friendly interpretation of "hard 100/session cap" while still -protecting against runaway loops. - -## Issue #156 — __resetForTests helper exported from prices namespace (LOW) - -The `prices.__resetForTests()` helper is exported alongside `fetchPrice`. This avoids -the need for `vi.resetModules()` + dynamic import between tests, which is flakier and -slower. The helper is named with `__` prefix to signal test-only usage. Alternative -considered: module-level export — rejected because it would pollute the public API -surface of balance.service outside the prices namespace. - -## Issue #158 — no @testing-library/react or jsdom in project (MEDIUM) - -The project has no `@testing-library/react`, no jsdom, and no happy-dom configured in vitest. -PriceFetchControl.test.tsx therefore uses direct unit tests of the component's internal -logic (hook calls, service calls, state transitions) via mocked dependencies rather than -DOM rendering. This tests behavior but not DOM structure. If the project later adopts RTL, -these tests should be rewritten with render() + getByRole/getByText. Not adding RTL in -autopilot mode (would require npm install permission). - -## Issue #158 — user_preferences table is per-profile by architecture (LOW) - -Each profile has its own SQLite database file (confirmed in ProfileContext). The -`user_preferences` table has no `profile_id` column — the profile scoping is implicit -(each DB = one profile). Therefore the consent key `price_fetching_consent` does NOT -need a profile_id prefix; the key alone is sufficient for correct per-profile scoping. - -## Issue #158 — asset_type not in category schema (MEDIUM) - -The `balance_categories` table has no `asset_type` column (confirmed by schema.sql). -The PriceFetchControl receives `assetType` prop from the parent. SnapshotLineRow/SnapshotEditor -do not yet carry asset_type. Per autopilot decision policy, defaulting to `'stock'` as -hardcoded fallback in SnapshotEditor wiring. A `// TODO: asset_type from category schema` -comment marks the injection point. A follow-up issue should add `asset_type TEXT DEFAULT 'stock'` -to `balance_categories` and thread it through SnapshotEditor props. - -## Issue #159 — deletePreference added to userPreferenceService (LOW) - -The `userPreferenceService` had no `deletePreference` function. Added a simple DELETE -SQL helper alongside `getPreference`/`setPreference`. Revoke must DELETE the row entirely -(not set value to null) so that `PriceFetchControl` re-shows the consent modal on next click -(it checks `getPreference` returning a truthy value). Setting to null would break that check. - -## Issue #159 — no Switch/Toggle component in shared/ (MEDIUM) - -No reusable Switch or Toggle component exists in `src/components/shared/`. A minimal -toggle was built inline inside `PriceFetchConsentToggle` using a styled `