"use client"; import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { ArrowUpCircle, BookOpen, Compass, ExternalLink, GitBranch, Info, Loader2, Sparkles, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useAuth } from "@/hooks/use-auth"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { restartOnboardingTour } from "@/lib/onboarding/tour-restart"; import { queryKeys } from "@/lib/query-keys"; import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch"; interface VersionPayload { version: string; buildSha: string | null; builtAt: string | null; license: string; repository: string; changelog: string; docs: string; } type CheckUpdatesResult = | { status: "up_to_date"; current: string; latest_tag: string; checked_at: string; } | { status: "newer_available"; current: string; latest_tag: string; html_url: string | null; published_at: string | null; checked_at: string; } | { status: "unknown"; current: string; reason: string }; const LAST_CHECKED_KEY = "healthlog-about-last-checked-iso"; // One day between auto-checks. We don't want every page-mount to spend a // GitHub-API rate-limit slot just to confirm the version is unchanged // 30 seconds after the previous one resolved. The user can always force // a re-check via the button. const AUTO_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; function readLastCheckedISO(): string | null { if (typeof window === "undefined") return null; try { return window.localStorage.getItem(LAST_CHECKED_KEY); } catch { return null; } } function writeLastCheckedISO(iso: string): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(LAST_CHECKED_KEY, iso); } catch { /* storage may be full or disabled — silently skip */ } } /** * v1.4.36 QA H7 — keyboard- and touch-accessible update badge. * * Renders as a 44 px-square hit target (anchor when a release URL is * known, span fallback otherwise) with a focus-visible ring, semantic * `text-primary` colour, and a real `aria-label` describing the * available version. Exported so the SSR test can pin the contract * without driving the parent's auto-check effect. */ export function UpdateBadge({ latestTag, htmlUrl, ariaLabel, }: { latestTag: string; htmlUrl: string | null; ariaLabel: string; }) { if (htmlUrl) { return ( {latestTag} ); } return ( ); } export function AboutSection() { const { t } = useTranslations(); const fmt = useFormatters(); // Pull the auth user — only needed so the "Replay the tour" button // can stamp a per-user force-launch marker into sessionStorage. The // hook is cheap (a cached `/api/auth/me` read) and every settings // page already lives behind the same auth boundary. const { user } = useAuth(); const { data: version, isLoading } = useQuery({ queryKey: queryKeys.apiVersion(), queryFn: async () => { return apiGet("/api/version"); }, // The endpoint is `force-static` — version doesn't change at runtime. staleTime: Infinity, }); // v1.4.47 W5 — onboarding chain-gate: the spotlight tour now auto- // launches only ≥ 24 h after the wizard finishes (see // `shouldAutoLaunchTour` in `components/onboarding/tour-launcher.tsx`). // The carousel + tour no longer stack into ~90 s of forced onboarding // on first visit. The tradeoff: a first-day user who *does* want the // tour needs a discoverable manual trigger. Settings → About is the // surface every user can find (every locale puts "About" right on the // settings shell), so it carries the replay button next to the // sources/docs links. Settings → Account still has its own // "Restart onboarding tour" — both buttons share the same flow. const [replayingTour, setReplayingTour] = useState(false); // v1.4.48 M6c — text + type collapsed into one discriminated state so // the two values can never drift (a "success" message rendered in the // destructive colour was the latent bug class). const [tourFeedback, setTourFeedback] = useState<{ text: string; type: "success" | "error"; } | null>(null); async function handleReplayTour() { setReplayingTour(true); setTourFeedback(null); // v1.4.48 M6b — both Settings → Account and Settings → About now // delegate to the shared `restartOnboardingTour()` worker so the // server flip + force-launch marker + window event live in one // place. The helper returns a discriminated result; this surface // only owns the translation + feedback rendering. const result = await restartOnboardingTour(user?.id); if (result.ok) { setTourFeedback({ text: t("onboarding.tour.restartConfirmation"), type: "success", }); } else { setTourFeedback({ text: t(result.messageKey), type: "error" }); } setReplayingTour(false); } // v1.4.36 W4f — the explicit "Check for updates" button is gone; // only the 24 h auto-check stays. When the auto-check reports // `newer_available`, a subtle upward-arrow badge appears next to // the version line with the latest tag in its tooltip. const [updateResult, setUpdateResult] = useState( null, ); async function runCheck(): Promise { try { const res = await apiFetchRaw("/api/version/check-updates"); if (!res.ok) { setUpdateResult({ status: "unknown", current: version?.version ?? "", reason: `http_${res.status}`, }); return; } const json = await res.json(); const result = json.data as CheckUpdatesResult; setUpdateResult(result); if (result.status !== "unknown") { writeLastCheckedISO(new Date().toISOString()); } } catch { setUpdateResult({ status: "unknown", current: version?.version ?? "", reason: "network_error", }); } } // Auto-check on mount when the last successful check is older than the // refresh interval (or has never run). Keeps the badge fresh without a // click — but won't spam GitHub on every navigation. // The setState inside runCheck() is intentional: the check is the // whole point of the effect, and the staleness gate prevents a // cascading-render loop because the timestamp resets after the run. /* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */ useEffect(() => { if (!version) return; const last = readLastCheckedISO(); const lastMs = last ? Date.parse(last) : NaN; const stale = Number.isNaN(lastMs) || Date.now() - lastMs > AUTO_CHECK_INTERVAL_MS; if (stale) { void runCheck(); } }, [version?.version]); /* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */ return (

{t("settings.sections.about.title")}

{t("settings.sections.about.description")}

{/* Identity card — version + license inline. The v1.4.2 layout stacked the license under the version inside its own boxed Badge, which read as a separate field even though the two values belong on the same row. */}
{isLoading || !version ? (
{t("common.loading")}
) : (
{t("settings.about.version")}
v{version.version}
{/* v1.4.36 W4f — subtle upward-arrow badge that appears only when the 24 h auto-check reports a newer release. Hover/tap reveals the latest tag via the title attribute (read by screen readers + as a native tooltip on desktop, and announced inline as the link copy on mobile). No bouncing, no badge counter, no manual recheck button — the badge is the entire surface. */} {updateResult?.status === "newer_available" && ( )}
{t("settings.about.license")}
{version.license}
{version.buildSha && (
{t("settings.about.gitSha")}
{version.buildSha.slice(0, 7)}
)} {version.builtAt && (
{t("settings.about.builtAtLabel")}
{fmt.date(version.builtAt)}
)}
)}
{/* Sources & docs — every link card was untitled in the v1.4.2 About surface, so the buttons floated alone with no heading telling the user what the section was for. */} {version && (
)} {/* v1.4.47 W5 — Replay tour card. The dashboard spotlight tour now auto-launches only ≥ 24 h after the wizard finishes, so a first-day user who wants to see it still needs a discoverable manual trigger. About is the surface every user can find (it's the standard "where am I?" stop in the settings shell), so the button lives here next to version + links. Mirrors the stack-on-mobile / right-align-on-desktop contract used by the Account section's "Restart onboarding tour" card so the two surfaces feel consistent. */}

{t("settings.about.tourReplay")}

{t("settings.about.tourReplayHint")}

{tourFeedback && (

{tourFeedback.text}

)}
{/* v1.4.36 W4f — the dedicated "Updates" card with the manual "Check for updates" button is gone. The 24 h auto- check still runs on mount and writes its result into the local `updateResult` state; the consequence renders as the subtle upward-arrow badge next to the version line above rather than as a separate panel. The card-level last-check timestamp, success / failure states, and the explicit button were collectively never the point of the surface — the only signal users ever cared about was "is a newer release out?", and the badge answers that with no clicks. */}
); }