diff --git a/src/components/settings/__tests__/about-section-update-badge.test.tsx b/src/components/settings/__tests__/about-section-update-badge.test.tsx
new file mode 100644
index 000000000..f40e79798
--- /dev/null
+++ b/src/components/settings/__tests__/about-section-update-badge.test.tsx
@@ -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(
+
+ {node}
+ ,
+ );
+}
+
+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(" — update badge", () => {
+ it("renders the version line without a badge by default", () => {
+ const html = render();
+ 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();
+ // 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();
+ expect(html).toContain("Sources & docs");
+ });
+});
diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx
index 170f0c6f1..9c2bfc534 100644
--- a/src/components/settings/__tests__/sections.test.tsx
+++ b/src/components/settings/__tests__/sections.test.tsx
@@ -132,16 +132,17 @@ describe("settings sections — SSR smoke", () => {
expect(html).not.toContain("onboarding.tour.");
});
- it(" renders version + license + check-for-updates button", () => {
+ it(" renders version + license + sources/docs cards (no Updates panel)", () => {
const html = render();
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 & 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(" resolves about.* keys in German", () => {
const html = render(, "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 & Dokumentation");
+ expect(html).not.toContain(">Jetzt prüfen<");
});
it(" renders heading and embeds both editors (v1.4.34 IW-D merge)", () => {
diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx
index 7831f9261..2f9a8a866 100644
--- a/src/components/settings/about-section.tsx
+++ b/src/components/settings/about-section.tsx
@@ -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(
null,
);
- const [lastCheckedISO, setLastCheckedISO] = useState(() =>
- readLastCheckedISO(),
- );
async function runCheck(): Promise {
- 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")}
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" &&
+ (updateResult.html_url ? (
+
+
+
+ ) : (
+
+
+
+ ))}
@@ -262,91 +292,16 @@ export function AboutSection() {
)}
- {/* 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 && (
-
- )}
+ {/* 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. */}
);
}