mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-dump
This commit is contained in:
commit
adc96fde3e
56 files changed
+719
-390
No files matched your search
@@ -99,7 +99,7 @@ export function BackupsSection() {
|
||||
{/* External docs link — `noopener noreferrer` because this
|
||||
leaves the admin shell. */}
|
||||
<a
|
||||
href="https://docs.healthlog.dev/admin/backups"
|
||||
href="https://docs.healthlog.dev/guides/backups/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary inline-flex items-center gap-1 underline-offset-2 hover:underline"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
const record = vi.hoisted(() => ({ inSharedRecord: false }));
|
||||
vi.mock("@/hooks/use-record-capabilities", () => ({
|
||||
useRecordCapabilities: () => ({ inSharedRecord: record.inSharedRecord }),
|
||||
}));
|
||||
import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents";
|
||||
import {
|
||||
AiUnavailableHint,
|
||||
@@ -57,10 +62,26 @@ describe("<AiUnavailableHint>", () => {
|
||||
expect(html).not.toContain('role="alert"');
|
||||
});
|
||||
|
||||
it("nudges toward local OCR when a text-only provider needs it", () => {
|
||||
it("points at the on-device scan switch when a text-only provider needs it", () => {
|
||||
const html = render(<AiUnavailableHint reason="enable-local-ocr" />);
|
||||
expect(html).toContain("local OCR");
|
||||
expect(html).toContain('href="/settings/ai"');
|
||||
// The switch lives on the Labs module page, under its own label.
|
||||
expect(html).toContain("Read scans on this device");
|
||||
expect(html).toContain("Settings → Modules → Labs");
|
||||
expect(html).toContain('href="/settings/modules/labs#labs-local-ocr"');
|
||||
expect(html).toContain("Open lab settings");
|
||||
expect(html).not.toContain('href="/settings/ai"');
|
||||
});
|
||||
|
||||
it("keeps the sentence but withholds the Settings link inside somebody else's record", () => {
|
||||
record.inSharedRecord = true;
|
||||
try {
|
||||
const html = render(<AiUnavailableHint reason="enable-local-ocr" />);
|
||||
expect(html).toContain("Read scans on this device");
|
||||
expect(html).not.toContain('data-slot="assist-settings-link"');
|
||||
expect(html).not.toContain('href="/settings');
|
||||
} finally {
|
||||
record.inSharedRecord = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
const record = vi.hoisted(() => ({ inSharedRecord: false }));
|
||||
vi.mock("@/hooks/use-record-capabilities", () => ({
|
||||
useRecordCapabilities: () => ({ inSharedRecord: record.inSharedRecord }),
|
||||
}));
|
||||
import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents";
|
||||
import { DocumentAiSection } from "../document-ai-section";
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { AiRunPhase } from "@/hooks/use-ai-run";
|
||||
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
@@ -64,9 +65,13 @@ export function AiRunPhaseNote({
|
||||
}
|
||||
|
||||
/**
|
||||
* Calm pointer to the AI settings, shown when assist is unavailable. When the
|
||||
* document is already searchable (auto-indexed locally) the copy stays honest —
|
||||
* it is searchable, an AI provider only adds a richer read.
|
||||
* Calm pointer to the setting that would make assist available: the AI
|
||||
* settings when no provider is configured, the on-device scan switch on the
|
||||
* Labs module page when the provider cannot read images. Both live under
|
||||
* `/settings`, which is the caller's own and closed inside somebody else's
|
||||
* record, so the link is withheld there and the sentence stands alone. When
|
||||
* the document is already searchable (auto-indexed locally) the copy stays
|
||||
* honest — it is searchable, an AI provider only adds a richer read.
|
||||
*/
|
||||
export function AiUnavailableHint({
|
||||
reason,
|
||||
@@ -74,10 +79,11 @@ export function AiUnavailableHint({
|
||||
reason: "no-provider" | "enable-local-ocr" | null;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const body =
|
||||
reason === "enable-local-ocr"
|
||||
? t("documents.assist.unavailableLocalOcr")
|
||||
: t("documents.assist.unavailableBody");
|
||||
const { inSharedRecord } = useRecordCapabilities();
|
||||
const localOcr = reason === "enable-local-ocr";
|
||||
const body = localOcr
|
||||
? t("documents.assist.unavailableLocalOcr")
|
||||
: t("documents.assist.unavailableBody");
|
||||
return (
|
||||
<div
|
||||
data-slot="assist-unavailable"
|
||||
@@ -85,14 +91,25 @@ export function AiUnavailableHint({
|
||||
>
|
||||
<Sparkles className="mt-0.5 size-3.5 shrink-0" aria-hidden />
|
||||
<p className="min-w-0">
|
||||
{body}{" "}
|
||||
<Link
|
||||
href="/settings/ai"
|
||||
data-slot="assist-settings-link"
|
||||
className="text-primary font-medium underline-offset-4 hover:underline"
|
||||
>
|
||||
{t("documents.assist.unavailableAction")}
|
||||
</Link>
|
||||
{body}
|
||||
{!inSharedRecord && (
|
||||
<>
|
||||
{" "}
|
||||
<Link
|
||||
href={
|
||||
localOcr
|
||||
? "/settings/modules/labs#labs-local-ocr"
|
||||
: "/settings/ai"
|
||||
}
|
||||
data-slot="assist-settings-link"
|
||||
className="text-primary font-medium underline-offset-4 hover:underline"
|
||||
>
|
||||
{localOcr
|
||||
? t("documents.assist.unavailableLocalOcrAction")
|
||||
: t("documents.assist.unavailableAction")}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -251,8 +251,13 @@ describe("IntegrationsSection — one status display per card", () => {
|
||||
for (const provider of PROVIDERS) {
|
||||
expect(html).toContain(`data-testid="${provider}-setup-guide"`);
|
||||
expect(html).toContain(
|
||||
`href="https://docs.healthlog.dev/integrations/${provider}"`,
|
||||
`href="https://docs.healthlog.dev/guides/connect-${provider}/"`,
|
||||
);
|
||||
}
|
||||
// Garmin has no connector; its note goes straight to the section of the
|
||||
// Google Health guide that explains the route, not through a redirect.
|
||||
expect(html).toContain(
|
||||
'href="https://docs.healthlog.dev/guides/connect-google-health/#garmin-owners"',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -139,7 +139,7 @@ describe("<ManagedIntegrationStatus>", () => {
|
||||
"Services you connect here send their data to Managed record's record, not to yours.",
|
||||
);
|
||||
expect(html).toContain(
|
||||
'href="https://docs.healthlog.dev/self-hosting/account-sharing#records-with-no-login"',
|
||||
'href="https://docs.healthlog.dev/guides/managed-profiles/#connected-services"',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("<FitbitRetirementNotice>", () => {
|
||||
expect(html).toContain("Google Health reads the same Fitbit and Pixel");
|
||||
expect(html).toContain("Everything already imported stays.");
|
||||
expect(html).toContain('href="#google-health"');
|
||||
expect(html).toContain("/google-health");
|
||||
expect(html).toContain("/guides/connect-google-health/");
|
||||
});
|
||||
|
||||
it("says the connection has stopped once the day has come", () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ExternalLink, Watch } from "lucide-react";
|
||||
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { INTEGRATION_DOCS_BASE } from "@/components/settings/integrations/setup-guide-link";
|
||||
import { GARMIN_DOCS_HREF } from "@/components/settings/integrations/setup-guide-link";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export function GarminInfoNote() {
|
||||
@@ -25,7 +25,7 @@ export function GarminInfoNote() {
|
||||
<p className="text-muted-foreground">
|
||||
{t("settings.garminInfo.body")}{" "}
|
||||
<a
|
||||
href={`${INTEGRATION_DOCS_BASE}/garmin`}
|
||||
href={GARMIN_DOCS_HREF}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="garmin-docs-link"
|
||||
|
||||
@@ -17,11 +17,16 @@ import { ExternalLink } from "lucide-react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* Base for every provider setup runbook. The provider key is appended as a
|
||||
* path segment, e.g. `https://docs.healthlog.dev/integrations/whoop`. Kept as
|
||||
* a single constant so the host never drifts across cards.
|
||||
* The docs site's guides, where every provider runbook lives at
|
||||
* `connect-<provider>/`, e.g. `https://docs.healthlog.dev/guides/connect-whoop/`.
|
||||
* These are the pages themselves: the older `/integrations/<provider>` paths
|
||||
* only resolve through redirects. Kept as a single constant so the host never
|
||||
* drifts across cards.
|
||||
*/
|
||||
export const INTEGRATION_DOCS_BASE = "https://docs.healthlog.dev/integrations";
|
||||
export const DOCS_GUIDES_BASE = "https://docs.healthlog.dev/guides";
|
||||
|
||||
/** Garmin has no connector; the Google Health guide explains the route. */
|
||||
export const GARMIN_DOCS_HREF = `${DOCS_GUIDES_BASE}/connect-google-health/#garmin-owners`;
|
||||
|
||||
export type IntegrationDocsProvider =
|
||||
| "whoop"
|
||||
@@ -30,15 +35,12 @@ export type IntegrationDocsProvider =
|
||||
| "polar"
|
||||
| "oura"
|
||||
| "nightscout"
|
||||
// v1.27.0 — Google Health runbook (docs.healthlog.dev/integrations/google-health).
|
||||
| "google-health"
|
||||
// v1.28.x — Strava runbook (docs.healthlog.dev/integrations/strava).
|
||||
| "strava"
|
||||
// v1.43 — Dawarich runbook (docs.healthlog.dev/integrations/dawarich).
|
||||
| "dawarich";
|
||||
|
||||
export function integrationDocsHref(provider: IntegrationDocsProvider): string {
|
||||
return `${INTEGRATION_DOCS_BASE}/${provider}`;
|
||||
return `${DOCS_GUIDES_BASE}/connect-${provider}/`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,7 +54,7 @@ const INTEGRATION_DISPLAY_NAMES = {
|
||||
|
||||
/** Where the guardian boundary is explained, connected services included. */
|
||||
const SHARING_DOCS_HREF =
|
||||
"https://docs.healthlog.dev/self-hosting/account-sharing#records-with-no-login";
|
||||
"https://docs.healthlog.dev/guides/managed-profiles/#connected-services";
|
||||
|
||||
function iconFor(key: ManagedIntegrationKey) {
|
||||
if (key === "nightscout") return Droplet;
|
||||
|
||||
Reference in new issue
Block a user