mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Code-review and design follow-ups before the v1.22 cut. No behaviour change
beyond the items below.
Coach / insights:
- Coach SSE route: a non-provider error raised inside the open producer (e.g. a
DB failure persisting the reply) now records on the wide event and forwards to
GlitchTip instead of masquerading as a provider outage; it still surfaces an
error frame since the stream is open.
- Lab ↔ outcome correlations: hedge faint-tier pairs ("a faint hint, if
anything … too small to lean on") so the narrated confidence never outruns the
effect, matching the daily engine.
- Reminder sweep: mint the plan-review reminder and clear the plan's reviewDate
in one transaction so a half-applied write can't duplicate the reminder.
- Fold the two raw provider-fallback runners onto one shared chain walker.
Design / accessibility:
- Lift sub-44px tap targets to the 44px mobile floor (suggested-action card,
Coach bubble affordances), brighten the resting timestamp clock for non-text
contrast, and route the lab chart height through the shared constant.
- Replace ad-hoc arbitrary type sizes with the scale token and static size-11
icon buttons with the named icon-lg variant.
- Settings: add the missing About card-header descriptions, normalise the
low-stock numeric widths, and relax the notification-status rows from nested
cards to a divided list.
Housekeeping:
- Drop three unused exported types (and the now-orphaned source const), re-stamp
v1.21.5 comments to v1.22, and register the live-judge eval harness as a knip
entry so the script-only module resolves.
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
ArrowUpCircle,
|
|
BookOpen,
|
|
ExternalLink,
|
|
GitBranch,
|
|
Info,
|
|
Loader2,
|
|
Sparkles,
|
|
} from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { SettingsCard } from "@/components/settings/settings-card";
|
|
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
|
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
|
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 (
|
|
<a
|
|
href={htmlUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
aria-label={ariaLabel}
|
|
title={ariaLabel}
|
|
className="text-primary focus-visible:ring-ring inline-flex min-h-11 min-w-11 items-center justify-center rounded-md hover:opacity-80 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
>
|
|
<ArrowUpCircle className="h-4 w-4" />
|
|
<span className="sr-only">{latestTag}</span>
|
|
</a>
|
|
);
|
|
}
|
|
return (
|
|
<span
|
|
role="img"
|
|
aria-label={ariaLabel}
|
|
title={ariaLabel}
|
|
className="text-primary inline-flex min-h-11 min-w-11 items-center justify-center"
|
|
>
|
|
<ArrowUpCircle className="h-4 w-4" />
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function AboutSection() {
|
|
const { t } = useTranslations();
|
|
const fmt = useFormatters();
|
|
|
|
const { data: version, isLoading } = useQuery({
|
|
queryKey: queryKeys.apiVersion(),
|
|
queryFn: async () => {
|
|
return apiGet<VersionPayload>("/api/version");
|
|
},
|
|
// The endpoint is `force-static` — version doesn't change at runtime.
|
|
staleTime: Infinity,
|
|
});
|
|
|
|
// 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,
|
|
);
|
|
|
|
async function runCheck(): Promise<void> {
|
|
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 */
|
|
|
|
// v1.18.6 (W9) — the visible heading + subtitle now come from the section
|
|
// frame that wraps this component: `<SettingsSectionFrame>` on the settings
|
|
// route and the admin renderer's `SectionFrame` on `/admin/about`. The body
|
|
// is the version / license / sources cards.
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 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. */}
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={Info}
|
|
title="HealthLog"
|
|
description={t("settings.about.identityDescription")}
|
|
className="mb-4"
|
|
/>
|
|
|
|
{isLoading || !version ? (
|
|
<div className="flex items-center gap-2 pl-7">
|
|
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
<span className="text-muted-foreground text-sm">
|
|
{t("common.loading")}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<dl className="flex flex-wrap items-baseline gap-x-6 gap-y-3 pl-7 text-sm">
|
|
<div className="flex items-baseline gap-2">
|
|
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
|
{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" && (
|
|
<UpdateBadge
|
|
latestTag={updateResult.latest_tag}
|
|
htmlUrl={updateResult.html_url}
|
|
ariaLabel={t("settings.about.newerAvailable", {
|
|
tag: updateResult.latest_tag,
|
|
})}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-baseline gap-2">
|
|
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
|
{t("settings.about.license")}
|
|
</dt>
|
|
<dd className="font-mono">{version.license}</dd>
|
|
</div>
|
|
|
|
{version.buildSha && (
|
|
<div className="flex items-baseline gap-2">
|
|
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
|
{t("settings.about.gitSha")}
|
|
</dt>
|
|
<dd className="font-mono">{version.buildSha.slice(0, 7)}</dd>
|
|
</div>
|
|
)}
|
|
|
|
{version.builtAt && (
|
|
<div className="flex items-baseline gap-2">
|
|
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
|
{t("settings.about.builtAtLabel")}
|
|
</dt>
|
|
<dd className="text-muted-foreground">
|
|
{fmt.date(version.builtAt)}
|
|
</dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
)}
|
|
</SettingsCard>
|
|
|
|
{/* 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 && (
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
icon={BookOpen}
|
|
title={t("settings.about.linksHeading")}
|
|
description={t("settings.about.linksDescription")}
|
|
className="mb-4"
|
|
/>
|
|
<div className="flex flex-col gap-2 pl-7 sm:flex-row sm:flex-wrap">
|
|
<Button
|
|
asChild
|
|
variant="outline"
|
|
size="sm"
|
|
className="min-h-11 sm:min-h-9"
|
|
>
|
|
<a
|
|
href={version.repository}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<GitBranch className="h-4 w-4" />
|
|
{t("settings.about.repository")}
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</a>
|
|
</Button>
|
|
<Button
|
|
asChild
|
|
variant="outline"
|
|
size="sm"
|
|
className="min-h-11 sm:min-h-9"
|
|
>
|
|
<a
|
|
href={version.changelog}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Sparkles className="h-4 w-4" />
|
|
{t("settings.about.changelog")}
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</a>
|
|
</Button>
|
|
<Button
|
|
asChild
|
|
variant="outline"
|
|
size="sm"
|
|
className="min-h-11 sm:min-h-9"
|
|
>
|
|
<a href={version.docs} target="_blank" rel="noopener noreferrer">
|
|
<BookOpen className="h-4 w-4" />
|
|
{t("settings.about.docs")}
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
</SettingsCard>
|
|
)}
|
|
|
|
{/* v1.18.6 — the tour-replay card consolidated onto a single home in
|
|
Settings → Advanced ("Modul-Tour neu starten"). The About surface
|
|
no longer duplicates it; per-module "Diese Tour zeigen" affordances
|
|
on every module page cover in-context re-entry. */}
|
|
|
|
{/* 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. */}
|
|
</div>
|
|
);
|
|
}
|