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. */}
+