mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(settings): never report a failed consent read as a withdrawal
The AI consent card turned any failed read of the receipt (a rate limit, a network drop) into "Withdrawn" and offered a grant. It now says the read failed, with a retry. The Coach history note is one sentence.
This commit is contained in:
1 parent
a6c65bd37e
commit
e72441743f
9 files changed
+52
-9
No files matched your search
+1
-1
@@ -2947,7 +2947,7 @@
|
||||
"note": "Diese Antwort wurde unterbrochen, bevor sie fertig war.",
|
||||
"retry": "Erneut fragen"
|
||||
},
|
||||
"historyReadOnlyNote": "Der Coach ist gerade nicht verfügbar. Deine Gespräche kannst du trotzdem lesen und löschen.",
|
||||
"historyReadOnlyNote": "Der Coach ist gerade nicht verfügbar, deine Gespräche kannst du aber weiter lesen und löschen.",
|
||||
"transcriptYou": "Du",
|
||||
"transcriptCoach": "Coach",
|
||||
"errorConsent": "Der Coach braucht deine KI-Einwilligung, bevor er antworten kann. Du kannst sie unter Einstellungen, KI geben.",
|
||||
|
||||
+1
-1
@@ -2947,7 +2947,7 @@
|
||||
"note": "This answer was interrupted before it finished.",
|
||||
"retry": "Ask again"
|
||||
},
|
||||
"historyReadOnlyNote": "The Coach is not available right now. You can still read and delete your conversations.",
|
||||
"historyReadOnlyNote": "The Coach is not available right now, but you can still read and delete your conversations.",
|
||||
"transcriptYou": "You",
|
||||
"transcriptCoach": "Coach",
|
||||
"errorConsent": "The Coach needs your AI consent before it can answer. You can give it in Settings, AI.",
|
||||
|
||||
+1
-1
@@ -2947,7 +2947,7 @@
|
||||
"note": "Esta respuesta se interrumpió antes de terminar.",
|
||||
"retry": "Preguntar de nuevo"
|
||||
},
|
||||
"historyReadOnlyNote": "El Coach no está disponible ahora mismo. Aun así puedes leer y eliminar tus conversaciones.",
|
||||
"historyReadOnlyNote": "El Coach no está disponible ahora mismo, pero aún puedes leer y eliminar tus conversaciones.",
|
||||
"transcriptYou": "Tú",
|
||||
"transcriptCoach": "Coach",
|
||||
"errorConsent": "El Coach necesita tu consentimiento de IA antes de poder responder. Puedes darlo en Ajustes, IA.",
|
||||
|
||||
+1
-1
@@ -2947,7 +2947,7 @@
|
||||
"note": "Cette réponse a été interrompue avant la fin.",
|
||||
"retry": "Reposer la question"
|
||||
},
|
||||
"historyReadOnlyNote": "Le Coach n'est pas disponible pour le moment. Vous pouvez toujours lire et supprimer vos conversations.",
|
||||
"historyReadOnlyNote": "Le Coach n'est pas disponible pour le moment, mais vous pouvez toujours lire et supprimer vos conversations.",
|
||||
"transcriptYou": "Vous",
|
||||
"transcriptCoach": "Coach",
|
||||
"errorConsent": "Le Coach a besoin de votre consentement à l'IA avant de pouvoir répondre. Vous pouvez le donner dans Réglages, IA.",
|
||||
|
||||
+1
-1
@@ -2947,7 +2947,7 @@
|
||||
"note": "Questa risposta è stata interrotta prima di essere completata.",
|
||||
"retry": "Chiedi di nuovo"
|
||||
},
|
||||
"historyReadOnlyNote": "Il Coach non è disponibile al momento. Puoi comunque leggere ed eliminare le tue conversazioni.",
|
||||
"historyReadOnlyNote": "Il Coach non è disponibile al momento, ma puoi ancora leggere ed eliminare le tue conversazioni.",
|
||||
"transcriptYou": "Tu",
|
||||
"transcriptCoach": "Coach",
|
||||
"errorConsent": "Il Coach ha bisogno del tuo consenso all'IA prima di poter rispondere. Puoi darlo in Impostazioni, IA.",
|
||||
|
||||
+1
-1
@@ -2947,7 +2947,7 @@
|
||||
"note": "이 답변은 끝나기 전에 중단됐어요.",
|
||||
"retry": "다시 물어보기"
|
||||
},
|
||||
"historyReadOnlyNote": "지금은 코치를 사용할 수 없습니다. 대화는 계속 읽고 삭제할 수 있습니다.",
|
||||
"historyReadOnlyNote": "지금은 코치를 사용할 수 없지만 대화는 계속 읽고 삭제할 수 있습니다.",
|
||||
"transcriptYou": "나",
|
||||
"transcriptCoach": "코치",
|
||||
"errorConsent": "코치가 답변하려면 AI 동의가 필요합니다. 설정의 AI에서 동의할 수 있습니다.",
|
||||
|
||||
+1
-1
@@ -2947,7 +2947,7 @@
|
||||
"note": "Ta odpowiedź została przerwana przed ukończeniem.",
|
||||
"retry": "Zapytaj ponownie"
|
||||
},
|
||||
"historyReadOnlyNote": "Coach jest teraz niedostępny. Nadal możesz czytać i usuwać swoje rozmowy.",
|
||||
"historyReadOnlyNote": "Coach jest teraz niedostępny, ale nadal możesz czytać i usuwać swoje rozmowy.",
|
||||
"transcriptYou": "Ty",
|
||||
"transcriptCoach": "Coach",
|
||||
"errorConsent": "Coach potrzebuje Twojej zgody na AI, zanim odpowie. Możesz ją wyrazić w Ustawieniach, AI.",
|
||||
|
||||
@@ -75,6 +75,35 @@ describe("AiConsentCard", () => {
|
||||
expect(html).toContain('data-slot="ai-consent-grant"');
|
||||
});
|
||||
|
||||
it("never reports a failed read as a withdrawal", () => {
|
||||
// A refused or failed read (the consent bucket's rate limit, a network
|
||||
// drop) says nothing about the decision. Reporting it as "Withdrawn" and
|
||||
// offering a grant would misstate the person's own choice.
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, retryOnMount: false } },
|
||||
});
|
||||
client
|
||||
.getQueryCache()
|
||||
.build(client, { queryKey: receiptKey })
|
||||
.setState({
|
||||
status: "error",
|
||||
error: new Error("read failed"),
|
||||
data: undefined,
|
||||
fetchStatus: "idle",
|
||||
});
|
||||
const html = renderToStaticMarkup(
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<AiConsentCard isAuthenticated />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(html).not.toContain("Withdrawn.");
|
||||
expect(html).not.toContain('data-slot="ai-consent-grant"');
|
||||
expect(html).not.toContain('data-slot="ai-consent-withdraw"');
|
||||
expect(html).toContain('data-slot="ai-consent-load-error"');
|
||||
});
|
||||
|
||||
it("never shows the grant control while consent stands", () => {
|
||||
const html = renderWith({
|
||||
id: "rcpt-1",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { Loader2, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { apiFetchRaw } from "@/lib/api/api-fetch";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -56,11 +57,13 @@ export function AiConsentCard({
|
||||
const queryClient = useQueryClient();
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: queryKeys.aiConsentReceipt("ai_full"),
|
||||
queryFn: async () => {
|
||||
const res = await apiFetchRaw("/api/consent/ai/latest?kind=ai_full");
|
||||
if (!res.ok) return null;
|
||||
// A failed read is not a withdrawal: throw so the card says it could
|
||||
// not load instead of reporting the person's decision wrongly.
|
||||
if (!res.ok) throw new Error("consent read failed");
|
||||
const json = await res.json();
|
||||
return (json.data?.receipt ?? null) as ConsentReceiptWire;
|
||||
},
|
||||
@@ -133,6 +136,17 @@ export function AiConsentCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<section className="space-y-3" data-slot="ai-consent">
|
||||
<QueryErrorRow
|
||||
slot="ai-consent-load-error"
|
||||
onRetry={() => void refetch()}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Narrow to the receipt itself rather than a boolean, so the branches
|
||||
// below can read its fields without an assertion.
|
||||
const activeReceipt = data && data.revokedAt === null ? data : null;
|
||||
|
||||
Reference in new issue
Block a user