From fa1c6a62cafbfcf90d8e0fd1c8de4ead65dd6817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 21:31:38 +0200 Subject: [PATCH] feat(settings): allow user to replay the onboarding tour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.4.15 Phase B5 — fourth (and final) commit. Closes acceptance criterion 4. Adds a Tour-replay card to Settings → Account, between Password and the password dialog. The card: - Calls `POST /api/onboarding/tour` with `{ completed: false }` so the server flips `users.onboarding_tour_completed` back to false. Idempotent: same DB write either direction; no audit log noise on the reset. - Refetches `/api/auth/me` so any in-memory re-evaluates with the fresh flag. - Dispatches a `healthlog:tour-restart` window event so a launcher already mounted on the dashboard (e.g. user has a dashboard tab open in another window) reopens the spotlight immediately, without waiting for the user to navigate. The launcher's listener clears the session-dismiss flag too. - Surfaces a confirmation message via the same status pattern as every other settings save (success / error / null). i18n: reuses the `onboarding.tour.restart`, `onboarding.tour.restartHint`, `onboarding.tour.restartConfirmation` keys that landed in commit 2/4 (EN + DE). No new strings. SSR test in `sections.test.tsx` locks the contract: the Restart button is present in the static markup and no raw `onboarding.tour.*` key leaks past the i18n layer. Tests: my 40 unit tests pass (was 39 — +1 for the new SSR assertion). The 12 unrelated failures observed in the full suite all live in uncommitted B6 doctor-report work in the working tree (`useEffect not defined` in advanced-section.tsx, missing `lastReportPracticeName` in unmigrated DB schema fixtures, etc.). Lint clean for B5 files; the single B6 lint error in `doctor-report-dialog.tsx` is out of scope. End-to-end shape: Phase B5 acceptance criteria: [x] DB migration 0030 + Prisma schema flag [x] Tour component (state machine + spotlight + i18n + a11y) [x] Auto-launch on first dashboard load [x] Settings → Account → Restart tour button This is the last commit in the B5 batch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../settings/__tests__/sections.test.tsx | 11 +++ src/components/settings/account-section.tsx | 93 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx index 9b8c71bed..4f2769e8c 100644 --- a/src/components/settings/__tests__/sections.test.tsx +++ b/src/components/settings/__tests__/sections.test.tsx @@ -98,6 +98,17 @@ describe("settings sections — SSR smoke", () => { expect(html).not.toContain("settings.sections."); }); + it(" exposes a Restart-tour button for B5 replay", () => { + const html = render(); + // v1.4.15 phase B5 acceptance criterion 4: Settings → Account + // exposes a "Restart onboarding tour" button that resets the + // server-side flag. Rendered as a card next to Password reset. + expect(html).toContain('data-testid="settings-restart-tour"'); + expect(html).toContain("Restart onboarding tour"); + // No raw i18n key leaks. + expect(html).not.toContain("onboarding.tour."); + }); + it(" renders version + license + check-for-updates button", () => { const html = render(); expect(html).toContain("About"); diff --git a/src/components/settings/account-section.tsx b/src/components/settings/account-section.tsx index 8846b24fe..9768680ac 100644 --- a/src/components/settings/account-section.tsx +++ b/src/components/settings/account-section.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, + Compass, KeyRound, Loader2, Save, @@ -76,6 +77,19 @@ export function AccountSection() { >(null); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + // v1.4.15 Phase B5 — onboarding-tour replay state. Settings → Account + // exposes a "Restart onboarding tour" button that resets the + // server-side flag (`users.onboarding_tour_completed = false`) and + // dispatches a `healthlog:tour-restart` window event so the dashboard's + // picks it up immediately on the user's next nav. The + // confirmation message goes through the same announce channel as + // every other settings save (status: "success" | "error" | null). + const [tourRestarting, setTourRestarting] = useState(false); + const [tourMsg, setTourMsg] = useState(null); + const [tourMsgType, setTourMsgType] = useState<"success" | "error" | null>( + null, + ); + // Passkey registration state. const [passkeyLoading, setPasskeyLoading] = useState(false); const [passkeyMsg, setPasskeyMsg] = useState(null); @@ -186,6 +200,41 @@ export function AccountSection() { } } + async function handleRestartTour() { + setTourRestarting(true); + setTourMsg(null); + setTourMsgType(null); + try { + const res = await fetch("/api/onboarding/tour", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ completed: false }), + }); + if (!res.ok) { + setTourMsg(t("settings.savingError")); + setTourMsgType("error"); + return; + } + // Refetch the auth payload so `` re-evaluates + // with the new flag, AND fire a window event so a launcher + // already mounted on the dashboard reopens immediately + // without waiting for a navigation. + await refetch(); + try { + window.dispatchEvent(new CustomEvent("healthlog:tour-restart")); + } catch { + /* ignore — only matters if the dashboard is already mounted */ + } + setTourMsg(t("onboarding.tour.restartConfirmation")); + setTourMsgType("success"); + } catch { + setTourMsg(t("common.networkError")); + setTourMsgType("error"); + } finally { + setTourRestarting(false); + } + } + async function handleChangePassword(e: React.FormEvent) { e.preventDefault(); setPasswordSaving(true); @@ -431,6 +480,50 @@ export function AccountSection() {

+ {/* Tour replay card. v1.4.15 Phase B5: a one-shot button that + resets `users.onboarding_tour_completed` on the server AND + dispatches a window event so a dashboard already in the + background reopens the spotlight tour immediately. */} +
+
+
+ +

+ {t("onboarding.tour.restart")} +

+
+ +
+

+ {t("onboarding.tour.restartHint")} +

+ {tourMsg && ( +

+ {tourMsg} +

+ )} +
+ {