From 4fe272d61582d6eaf56a5bdf40ac47a63c1ff14b Mon Sep 17 00:00:00 2001 From: Ivan Schneider Date: Sat, 25 Jul 2026 14:27:40 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20suivi=20=C3=A0=20distance=20?= =?UTF-8?q?=E2=80=94=20le=20parent=20consulte=20la=20progression=20depuis?= =?UTF-8?q?=20son=20appareil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le multi-profils couvre la tablette familiale partagée, mais pas le cas où l'enfant pratique sur un appareil qui n'est pas celui du parent (ancien téléphone en WiFi seul). Le tableau de bord parent existait déjà, mais il vivait sur l'appareil de l'enfant. L'appareil de l'enfant publie après chaque séance un instantané de son profil gzippé puis chiffré côté client (AES-GCM), sous un code de 96 bits. L'appareil du parent scanne le QR une fois et relit ce dépôt. Même primitive que le transfert : la clé ne vit que dans le fragment d'URL, jamais transmise au serveur. Deux différences, qui justifient une table à part : le dépôt est durable et la lecture non consommante. Le profil suivi n'est jamais installé chez le parent — installé, il apparaîtrait dans « Qui joue ? » et une séance faite dessus par erreur divergerait de l'appareil de l'enfant. Un parent sans Tablito est pris en charge de bout en bout : le fragment #watch= saute la landing statique et l'app ouvre directement l'espace parent, jamais l'onboarding enfant. L'espace parent affiche les deux sources en parallèle (profil local + enfants suivis) via un sélecteur en tête, le corps de stats étant extrait en ParentStats pour un rendu identique des deux côtés. - supabase/watches.sql : table + 3 RPC SECURITY DEFINER, RLS sans policy - lib/watchStore (localStorage, eager) / lib/watch (réseau+crypto, dynamique) pour ne pas faire entrer le chiffrement dans le graphe de modules du boot - supabaseRpc() remonté dans lib/supabase, adopté par transfer.ts - scanner QR extrait en hook useQrScan, réutilisé par WelcomeScreen - confidentialité (fr/en) : nouvelle section, 4e exception déclarée ; specs §14 - « par défaut » ajouté aux affirmations « les données restent sur l'appareil » --- index.html | 25 +- public/specs/index.html | 28 +- src/App.tsx | 54 +- src/__tests__/helpers/watchServer.ts | 57 ++ src/__tests__/remoteFollow.test.tsx | 231 +++++++ src/__tests__/setup.ts | 9 + src/__tests__/watch.test.ts | 223 +++++++ src/components/ParentStats.tsx | 346 +++++++++++ src/components/QrCanvas.tsx | 36 ++ src/hooks/useQrScan.ts | 73 +++ src/i18n/changelog.ts | 12 + src/i18n/lang.ts | 10 + src/i18n/onboarding.ts | 8 + src/i18n/parent.ts | 119 +++- src/i18n/privacy.tsx | 51 +- src/lib/supabase.ts | 20 + src/lib/transfer.ts | 74 ++- src/lib/watch.ts | 189 ++++++ src/lib/watchStore.ts | 117 ++++ src/main.tsx | 58 +- src/screens/ParentDashboard.css | 75 +++ src/screens/ParentDashboard.tsx | 861 ++++++++++++++------------- src/screens/PrivacyScreen.tsx | 5 + src/screens/WelcomeScreen.css | 6 +- src/screens/WelcomeScreen.tsx | 59 +- supabase/watches.sql | 117 ++++ 26 files changed, 2327 insertions(+), 536 deletions(-) create mode 100644 src/__tests__/helpers/watchServer.ts create mode 100644 src/__tests__/remoteFollow.test.tsx create mode 100644 src/__tests__/watch.test.ts create mode 100644 src/components/ParentStats.tsx create mode 100644 src/components/QrCanvas.tsx create mode 100644 src/hooks/useQrScan.ts create mode 100644 src/lib/watch.ts create mode 100644 src/lib/watchStore.ts create mode 100644 supabase/watches.sql diff --git a/index.html b/index.html index 67c9d393..786cdc5b 100644 --- a/index.html +++ b/index.html @@ -51,17 +51,20 @@ try { var standalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || window.navigator.standalone === true; - var profile = false, skipped = false, hasImport = false; + var profile = false, skipped = false, hasBootFragment = false; // Deux clés : `multiplix-profiles` (index multi-profils) et // `multiplix-profile` (ancien schéma mono-profil, migré au boot). try { profile = !!(localStorage.getItem('multiplix-profiles') || localStorage.getItem('multiplix-profile')); } catch (e) {} try { skipped = localStorage.getItem('multiplix-skip-install') === '1'; } catch (e) {} - // Profil dans le fragment d'URL → on boote l'app directement (pas la - // landing) : #import= (migration cross-origin depuis l'ancien domaine) - // ou #transfer= (transfert depuis un autre appareil, QR de l'espace - // parent). L'import réel a lieu dans main.tsx. - try { hasImport = location.hash.indexOf('import=') !== -1 || location.hash.indexOf('transfer=') !== -1; } catch (e) {} - if (standalone || profile || skipped || hasImport) { + // Fragment d'URL porteur d'un profil ou d'un appairage → on boote + // l'app directement (pas la landing) : #import= (migration + // cross-origin depuis l'ancien domaine), #transfer= (transfert depuis + // un autre appareil, QR de l'espace parent), ou #watch= (suivi à + // distance : un parent scanne le QR de l'appareil de son enfant, et + // n'a rien à faire de la landing). Le traitement réel a lieu dans + // main.tsx. + try { hasBootFragment = /(import|transfer|watch)=/.test(location.hash); } catch (e) {} + if (standalone || profile || skipped || hasBootFragment) { document.documentElement.classList.add('skip-static-landing'); } // Langue de l'interface (globale) : préférence explicite stockée, @@ -79,7 +82,7 @@ if (lang === 'en') { document.title = 'Tablito — Practise the times tables (free, no ads)'; var desc = document.querySelector('meta[name="description"]'); - if (desc) desc.setAttribute('content', 'Free, ad-free app to learn the multiplication tables gently, from age 7. Leitner method, no grades or rankings, works offline. Data stays on the device.'); + if (desc) desc.setAttribute('content', 'Free, ad-free app to learn the multiplication tables gently, from age 7. Leitner method, no grades or rankings, works offline. Data stays on the device by default.'); // Manifest localisé : le standard n'a pas de description multilingue, // donc un fichier par langue, basculé ici (le // existe déjà plus haut). Seule la `description` diffère ; le nom et @@ -123,7 +126,7 @@ media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" /> Tablito — Réviser les tables de multiplication (gratuit, sans pub) - +