ux(about): drop manual update-check panel for an inline badge

The dedicated "Updates" card carried a "Check for updates" button,
a last-checked timestamp, and three result paragraphs covering
up-to-date / newer-available / unknown. All of it surfaced an answer
to one question — "is there a newer release?" — and the answer was
overwhelmingly "no, you're current". The card was three full rem of
chrome for that signal.

The 24 h auto-check still runs on mount. When it returns
`newer_available`, an ArrowUpCircle badge appears next to the
version line; hover/tap exposes the latest tag via the title
attribute. The link points at the release HTML URL when present.
No bouncing, no badge counter, no manual recheck button.

The settings/about route now reads as a tight identity card +
external links list, matching how often the surface actually
gets used.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 12:39:09 +02:00
1 parent e203f07cbd
commit 0738ef50b0
3 files changed
+170 -111

No files matched your search

@@ -0,0 +1,102 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nProvider } from "@/lib/i18n/context";
import { AboutSection } from "../about-section";
/**
* v1.4.36 W4f — AboutSection update-badge contract.
*
* The dedicated "Updates" card with the manual "Check for updates"
* button is gone. The 24 h auto-check still runs on mount and
* surfaces a subtle ArrowUpCircle badge next to the version line
* ONLY when the check returns `{ status: "newer_available", ... }`.
*
* These SSR smoke tests pin the badge presence by seeding the
* react-query cache with a fake version payload (so the version
* line paints synchronously) and exercising the path that decides
* whether the badge mounts. The badge itself owns its own
* `useState`, so its presence is gated on the runCheck branch —
* SSR renders the initial `null` state. The actual badge render
* is therefore covered by the runtime store-set + re-render in
* future Playwright surfaces; here we pin the *baseline* contract
* that the version line + Sources/docs panel still render and
* the legacy manual-check chrome is gone.
*/
function makeClient() {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Number.POSITIVE_INFINITY,
refetchOnMount: false,
},
},
});
// Seed the version query so the section paints inline rather than
// rendering its loader skeleton.
client.setQueryData(["api", "version"], {
version: "1.4.36",
buildSha: "abc1234",
builtAt: "2026-05-17T08:00:00.000Z",
license: "AGPL-3.0",
repository: "https://github.com/MBombeck/HealthLog",
changelog: "https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md",
docs: "https://github.com/MBombeck/HealthLog/tree/main/docs",
});
return client;
}
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
const client = makeClient();
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>
<QueryClientProvider client={client}>{node}</QueryClientProvider>
</I18nProvider>,
);
}
beforeEach(() => {
vi.clearAllMocks();
// Fail the auto-check fetch so the badge stays in its initial
// (no-result) state. This lets the SSR smoke tests assert the
// "no badge" baseline; the badge-visible branch is unit-tested
// separately by toggling `updateResult` via a forced re-render
// in the future Playwright + RTL suite.
vi.stubGlobal(
"fetch",
vi.fn(async () => {
throw new Error("network disabled in tests");
}),
);
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe("<AboutSection> — update badge", () => {
it("renders the version line without a badge by default", () => {
const html = render(<AboutSection />);
expect(html).toContain("v1.4.36");
// No badge — ArrowUpCircle's lucide class name is the unique signal.
expect(html).not.toContain("lucide-arrow-up-circle");
});
it("drops the manual 'Check for updates' button surface entirely", () => {
const html = render(<AboutSection />);
// Pre-v1.4.36 the section carried a "Check for updates" CTA
// alongside a last-checked timestamp. Both are now gone — only
// the 24 h auto-check stays, surfacing through the badge above.
expect(html).not.toContain(">Check for updates<");
expect(html).not.toContain(">Check now<");
expect(html).not.toContain("last-checked-iso");
});
it("preserves the Sources & docs panel", () => {
const html = render(<AboutSection />);
expect(html).toContain("Sources &amp; docs");
});
});
@@ -132,16 +132,17 @@ describe("settings sections — SSR smoke", () => {
expect(html).not.toContain("onboarding.tour.");
});
it("<AboutSection> renders version + license + check-for-updates button", () => {
it("<AboutSection> renders version + license + sources/docs cards (no Updates panel)", () => {
const html = render(<AboutSection />);
expect(html).toContain("About");
// The new about.* keys resolve. v1.4.3 split the page into three
// titled cards (HealthLog identity, Sources & docs, Updates) and
// shortened the button label from "Check for updates" to "Check now"
// because the surrounding heading already names the section.
expect(html).toContain("Updates");
// v1.4.36 W4f — the dedicated "Updates" card with its manual
// "Check now" button is gone. The 24 h auto-check still runs and
// surfaces a subtle ArrowUpCircle badge next to the version line
// when a newer release is available. The Sources & docs card
// stays put.
expect(html).toContain("Sources &amp; docs");
expect(html).toContain("Check now");
expect(html).not.toContain(">Check now<");
expect(html).not.toContain(">Check for updates<");
expect(html).not.toContain("settings.about.");
});
@@ -198,11 +199,12 @@ describe("settings sections — SSR smoke", () => {
it("<AboutSection> resolves about.* keys in German", () => {
const html = render(<AboutSection />, "de");
// v1.4.3: button label shortened to "Jetzt prüfen" because the
// section heading "Updates" already provides context.
expect(html).toContain("Updates");
expect(html).toContain("Jetzt prüfen");
// v1.4.36 W4f — "Updates" panel + "Jetzt prüfen" button removed;
// the surviving German copy is the section title + the Sources
// & docs heading + the version line.
expect(html).toContain("Über");
expect(html).toContain("Quellen &amp; Dokumentation");
expect(html).not.toContain(">Jetzt prüfen<");
});
it("<ThresholdsSection> renders heading and embeds both editors (v1.4.34 IW-D merge)", () => {
+55 -100
View File
@@ -3,15 +3,13 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
ArrowUpCircle,
BookOpen,
CheckCircle2,
ExternalLink,
GitBranch,
Info,
Loader2,
RefreshCw,
Sparkles,
XCircle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -85,16 +83,15 @@ export function AboutSection() {
staleTime: Infinity,
});
const [checking, setChecking] = useState(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<CheckUpdatesResult | null>(
null,
);
const [lastCheckedISO, setLastCheckedISO] = useState<string | null>(() =>
readLastCheckedISO(),
);
async function runCheck(): Promise<void> {
setChecking(true);
try {
const res = await fetch("/api/version/check-updates");
if (!res.ok) {
@@ -109,9 +106,7 @@ export function AboutSection() {
const result = json.data as CheckUpdatesResult;
setUpdateResult(result);
if (result.status !== "unknown") {
const iso = new Date().toISOString();
writeLastCheckedISO(iso);
setLastCheckedISO(iso);
writeLastCheckedISO(new Date().toISOString());
}
} catch {
setUpdateResult({
@@ -119,14 +114,12 @@ export function AboutSection() {
current: version?.version ?? "",
reason: "network_error",
});
} finally {
setChecking(false);
}
}
// Auto-check on mount when the last successful check is older than the
// refresh interval (or has never run). Keeps the UI populated without
// requiring a click — but won't spam GitHub on every navigation.
// 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.
@@ -184,6 +177,43 @@ export function AboutSection() {
{t("settings.about.version")}
</dt>
<dd className="font-mono font-medium">v{version.version}</dd>
{/* 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" &&
(updateResult.html_url ? (
<a
href={updateResult.html_url}
target="_blank"
rel="noopener noreferrer"
aria-label={t("settings.about.newerAvailable", {
tag: updateResult.latest_tag,
})}
title={t("settings.about.newerAvailable", {
tag: updateResult.latest_tag,
})}
className="text-dracula-purple inline-flex items-baseline hover:opacity-80"
>
<ArrowUpCircle className="h-4 w-4 self-center" />
</a>
) : (
<span
aria-label={t("settings.about.newerAvailable", {
tag: updateResult.latest_tag,
})}
title={t("settings.about.newerAvailable", {
tag: updateResult.latest_tag,
})}
className="text-dracula-purple inline-flex items-baseline"
>
<ArrowUpCircle className="h-4 w-4 self-center" />
</span>
))}
</div>
<div className="flex items-baseline gap-2">
@@ -262,91 +292,16 @@ export function AboutSection() {
</div>
)}
{/* Updates — proper heading + last-check timestamp + manual button
that proxies through `/api/version/check-updates`. The v1.4.2
version called `api.github.com` directly from the browser, which
the production CSP blocked silently — that's why "nothing
happened" when the user clicked the button. */}
{version && (
<div className="bg-card border-border rounded-xl border p-6">
<div className="mb-4 flex items-center gap-2">
<RefreshCw className="text-primary h-5 w-5" />
<h2 className="text-lg font-semibold">
{t("settings.about.updatesHeading")}
</h2>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
type="button"
variant="outline"
onClick={runCheck}
disabled={checking}
>
{checking ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
{checking
? t("settings.about.checking")
: t("settings.about.checkUpdates")}
</Button>
<p className="text-muted-foreground text-xs">
{lastCheckedISO
? t("settings.about.lastChecked", {
time: fmt.dateTime(lastCheckedISO),
})
: t("settings.about.lastCheckedNever")}
</p>
</div>
{updateResult?.status === "up_to_date" && (
<p
role="status"
className="text-dracula-green mt-3 flex items-center gap-1.5 text-sm"
>
<CheckCircle2 className="h-4 w-4" />
{t("settings.about.upToDate")}
</p>
)}
{updateResult?.status === "newer_available" && (
<p role="status" className="mt-3 flex items-center gap-1.5 text-sm">
<Sparkles className="text-dracula-purple h-4 w-4" />
{updateResult.html_url ? (
<a
href={updateResult.html_url}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-offset-2 hover:underline"
>
{t("settings.about.newerAvailable", {
tag: updateResult.latest_tag,
})}
</a>
) : (
<span>
{t("settings.about.newerAvailable", {
tag: updateResult.latest_tag,
})}
</span>
)}
</p>
)}
{updateResult?.status === "unknown" && (
<p
role="alert"
className="text-destructive mt-3 flex items-center gap-1.5 text-sm"
>
<XCircle className="h-4 w-4" />
{t("settings.about.checkFailed")}
</p>
)}
</div>
)}
{/* 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. */}
</section>
);
}