diff --git a/src/components/settings/integrations/nightscout-card.tsx b/src/components/settings/integrations/nightscout-card.tsx index d5917f0c2..3008bed95 100644 --- a/src/components/settings/integrations/nightscout-card.tsx +++ b/src/components/settings/integrations/nightscout-card.tsx @@ -6,7 +6,8 @@ // card reads the consolidated `/api/integrations/status` envelope like every // sibling — it was the last card firing its own status round-trip, and the // last place a fourth status dialect could hide. Warm copy, mobile-first, the -// private-network opt-in toggle maps to `nightscoutAllowPrivateHost`. +// private-network access is operator-owned through an exact-origin server +// allowlist; ordinary users cannot weaken the outbound-request policy. import { useRef, useState } from "react"; import Link from "next/link"; @@ -36,7 +37,6 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PasswordInput } from "@/components/ui/password-input"; -import { Switch } from "@/components/ui/switch"; import { TagChip } from "@/components/ui/tag-chip"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; @@ -77,7 +77,6 @@ export function NightscoutCard({ const [url, setUrl] = useState(""); const [token, setToken] = useState(""); - const [allowPrivateHost, setAllowPrivateHost] = useState(false); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); const [msgType, setMsgType] = useState<"success" | "error" | null>(null); @@ -95,7 +94,6 @@ export function NightscoutCard({ onSuccess: () => { setUrl(""); setToken(""); - setAllowPrivateHost(false); // The card reads the consolidated envelope, so that is the one key a // disconnect has to invalidate for the pill to flip back. queryClient.invalidateQueries({ @@ -116,7 +114,6 @@ export function NightscoutCard({ body: JSON.stringify({ url: url.trim(), token: token.trim(), - allowPrivateHost, }), }); if (res.ok) { @@ -184,6 +181,8 @@ export function NightscoutCard({ } const pillState = pillStateForVerdict(status?.syncHealth?.verdict); + const operatorApprovalRequired = + status?.lastError?.startsWith("private_origin_not_approved") ?? false; // `warning` joins the set now that a transient failure paints amber rather // than red — the detail belongs on this line, not in a colour that tells the // user to reconnect against something reconnecting cannot fix. @@ -191,7 +190,8 @@ export function NightscoutCard({ (pillState === "error" || pillState === "parked" || pillState === "warning") && - status?.lastError + status?.lastError && + !operatorApprovalRequired ? status.lastError : null; @@ -223,6 +223,19 @@ export function NightscoutCard({
{errorMessage && } + {operatorApprovalRequired && ( +
+

+ Private Nightscout access requires server operator approval. +

+

+ Ask your server operator to approve this exact Nightscout origin. +

+
+ )} {/* Parked-integration resume CTA. Surfaces only when the row state is `parked` (>24h of persistent failures). Nightscout has no OAuth @@ -313,22 +326,6 @@ export function NightscoutCard({

-
-
- -

- {t("settings.nightscoutPrivateHostHelp")} -

-
- -
-
- + {!operatorApprovalRequired && ( + <> + + + + )}