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} +

+ )} +
+ {