mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
The v1.38.0 notice that warns when the configured OAuth callback origin
differs from the origin the browser is on never rendered in the published
image, and the copyable callback address in the redirect guide showed a
bare path. `integrationCallbackUrl()` read `process.env.NEXT_PUBLIC_APP_URL`
inside a "use client" module. Next.js inlines NEXT_PUBLIC_* into client
bundles at build time, and the GHCR image is built without that variable
(only the version, build-sha and built-at args reach the build), so the
base compiled to an empty string, `new URL("/api/<provider>/callback")`
threw, and the notice returned null on every card for every self-hoster.
The value now comes from the server at request time. A new map
(`src/lib/integrations/callback-urls.ts`) calls each provider's own
redirect-URI function (the four private `getRedirectUri()` helpers are
exported under provider-specific names; Polar, Oura and Strava already
exported theirs), so the page shows exactly what the handshake registers,
explicit `*_REDIRECT_URI` override included. The settings route resolves
the map and passes it as a prop through `IntegrationsSection` and
`ConnectionsPanel` to every card. Fitbit and Google Health validate their
URI and throw on a bad configuration; that surfaces as `null`, the notice
skips the comparison and the guide falls back to the fixed path.
Tests: a map test proving the explicit override wins and the derived form
is used otherwise, a component test rendering the notice from a passed
value (mismatch renders, match does not), and a structural guard that
fails when any "use client" file under src/components/settings/integrations
reads NEXT_PUBLIC_APP_URL.
104 lines
4.6 KiB
TypeScript
104 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* `<IntegrationsSection>` — Settings → Integrationen.
|
|
*
|
|
* v1.18.1 (D4) — the Connections content IS the Integrationen page: clicking
|
|
* Integrationen shows the OAuth + device data sources (Withings / WHOOP /
|
|
* Fitbit / Polar / Oura / Nightscout) directly, with no tab strip.
|
|
*
|
|
* v1.25.7 — the delivery CHANNELS (Telegram / ntfy / Web Push / Webhook /
|
|
* Email) move back here from Notifications as a labelled "Delivery channels"
|
|
* group below the connections. Channels are delivery providers — the same
|
|
* conceptual family as Withings / WHOOP / a connected device — not reminder
|
|
* preferences, so they belong with the other integrations. Notifications keeps
|
|
* only the reminder-TYPE content. The group carries `id="channels"` so
|
|
* `/settings/channels` 301-redirects to `/settings/integrations#channels`, and
|
|
* the per-channel anchors (`#telegram`, `#ntfy`, …) inside the panel keep
|
|
* working.
|
|
*
|
|
* `parseOAuthOutcome` / `oauthReasonKey` are re-exported from
|
|
* `connections-panel.tsx` so existing unit-test imports keep resolving.
|
|
*
|
|
* 2026-07-17 UX/IA audit M7 — `<NutrientIntakeCard>` (v1.28) moved here from
|
|
* Settings → Sources. It is a read-only sync inventory (what the Apple
|
|
* Health nutrients opt-in has stored), not a which-connection-wins ranking —
|
|
* "Source priority" describes the latter only. Matches the standing
|
|
* providers→Integrations rule; the card still renders nothing while the
|
|
* `nutrients` module is off.
|
|
*/
|
|
|
|
import Link from "next/link";
|
|
import { Link2, Send } from "lucide-react";
|
|
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls";
|
|
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
|
|
import { SectionHeading } from "@/components/ui/section-heading";
|
|
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
|
|
import { NutrientIntakeCard } from "@/components/settings/nutrient-intake-card";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export {
|
|
parseOAuthOutcome,
|
|
oauthReasonKey,
|
|
} from "@/components/settings/integrations/connections-panel";
|
|
|
|
export function IntegrationsSection({
|
|
callbackUrls,
|
|
}: {
|
|
/** Server-resolved per-provider OAuth callback URLs; see `ConnectionsPanel`. */
|
|
callbackUrls: IntegrationCallbackUrls;
|
|
}) {
|
|
const { t } = useTranslations();
|
|
|
|
// v1.18.6 (W9) — the visible page heading + subtitle come from the shared
|
|
// `<SettingsSectionFrame>` in the route; the connections panel is the
|
|
// primary surface, with the delivery-channels group sequenced below it.
|
|
return (
|
|
<div className="space-y-10">
|
|
{/* Connections ("where the data comes from"). The OAuth + device data
|
|
sources. A labelled group header gives this block parity with the
|
|
delivery-channels group below it. */}
|
|
<section className="space-y-4">
|
|
<SectionHeading
|
|
icon={Link2}
|
|
title={t("settings.sections.integrations.connectionsHeading")}
|
|
/>
|
|
<ConnectionsPanel callbackUrls={callbackUrls} />
|
|
|
|
{/* 2026-07-17 UX/IA audit M7 — the opt-in nutrients sync inventory
|
|
(moved from Settings → Sources, where "source priority" only
|
|
ever meant which-connection-wins ranking, not a read-only sync
|
|
list). The card carries its own header + renders nothing while
|
|
the `nutrients` module is off, so it slots in as a sibling card
|
|
under the same Connections group rather than a new labelled
|
|
section that could paint an orphaned heading over nothing. */}
|
|
<NutrientIntakeCard />
|
|
</section>
|
|
|
|
{/* Delivery channels ("where it's delivered"). The existing channels
|
|
panel, unchanged. `id="channels"` anchors the group for the
|
|
`/settings/channels` redirect; the per-channel anchors stay inside
|
|
the panel. */}
|
|
<section id="channels" className="scroll-mt-28 space-y-4">
|
|
<SectionHeading
|
|
icon={Send}
|
|
title={t("settings.sections.integrations.channelsHeading")}
|
|
subtitle={t("settings.sections.integrations.channelsDescription")}
|
|
/>
|
|
{/* Names the per-event notification matrix so the three notification
|
|
surfaces (matrix, reminder types, channels) cross-reference. */}
|
|
<div>
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href="/notifications">
|
|
{t("settings.sections.integrations.matrixLink")}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
<NotificationChannelsPanel />
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|