Merge branch 'fix/v14311-settings' into release/v1.43.11

# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/nl.json
#	messages/pl.json
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 14:13:51 +02:00
commit eabfeaef08
74 files changed
+2545 -436

No files matched your search

+4
View File
@@ -391,6 +391,10 @@ export const GET = apiHandler(async () => {
// resolved `ai.capabilities.coach` and `modules.coach` below, which fold
// in the record's own opt-out, the operator's switches and the grant.
disableCoach: user.disableCoach ?? false,
// The name the app addresses the account by (dashboard greeting,
// reports). Set in the setup flow and edited on the profile card, which
// seeds its field from here.
displayName: user.displayName ?? null,
// v1.7.0 — health-record export identity fields. All optional.
fullName: user.fullName ?? null,
insurerName: user.insurerName ?? null,
@@ -12,6 +12,7 @@ import { notificationPreferenceSchema } from "@/lib/validations/notifications";
import { EVENT_TYPES, CHANNEL_TYPE_LABELS } from "@/lib/notifications/types";
import type { ChannelType } from "@/lib/notifications/types";
import { decrypt, encrypt } from "@/lib/crypto";
import { resolveEventDefaults } from "@/lib/notifications/event-defaults";
export const GET = apiHandler(async () => {
const { user } = await requireAuth();
@@ -92,6 +93,12 @@ export const GET = apiHandler(async () => {
channels: channelData,
preferences,
eventTypes: EVENT_TYPES as unknown as string[],
// What an event does on a channel with no stored row, resolved for this
// account by the same function the dispatcher asks. A client renders a
// switch without a row from this map, never from its own copy of the
// defaults: the mood reminder's follows the reminder card, which a
// constant table cannot know.
eventDefaults: await resolveEventDefaults(user.id),
});
});
+1
View File
@@ -191,6 +191,7 @@ function CoachPlansBody({ readOnly }: { readOnly: boolean }) {
{(readOnly || group === "past") && (
<ConfirmButton
slot="coach-plan-delete"
destructive
variant="ghost"
size="sm"
className="min-h-9"
@@ -126,4 +126,58 @@ describe("NotificationsPage", () => {
const html = render();
expect(html).toContain("won’t receive notifications");
});
it("a switch without a stored row shows the server's default for this account", () => {
// The mood reminder is delivered when the reminder card is on, with no
// per-channel row. The constant table says OFF for it, so a matrix that
// read the table showed the switch off while the reminder kept arriving.
// The page renders what the server resolved, row-less events included.
queryState.data = {
channels: [
{
id: "c1",
type: "TELEGRAM",
label: "Telegram",
enabled: true,
globallyEnabled: true,
},
],
preferences: [],
eventTypes: ["MOOD_REMINDER", "PERSONAL_RECORD"],
eventDefaults: { MOOD_REMINDER: true, PERSONAL_RECORD: false },
};
const html = render();
const state = (label: string) =>
new RegExp(
`aria-label="${label} - Telegram"[^>]*data-state="(checked|unchecked)"|data-state="(checked|unchecked)"[^>]*aria-label="${label} - Telegram"`,
).exec(html);
const mood = state("Mood reminder");
const record = state("Personal record");
expect(mood, "mood switch rendered").not.toBeNull();
expect(mood?.[1] ?? mood?.[2]).toBe("checked");
expect(record?.[1] ?? record?.[2]).toBe("unchecked");
});
it("a stored row still wins over the default", () => {
queryState.data = {
channels: [
{
id: "c1",
type: "TELEGRAM",
label: "Telegram",
enabled: true,
globallyEnabled: true,
},
],
preferences: [
{ channelId: "c1", eventType: "MOOD_REMINDER", enabled: false },
],
eventTypes: ["MOOD_REMINDER"],
eventDefaults: { MOOD_REMINDER: true },
};
const html = render();
expect(html).toMatch(
/aria-label="Mood reminder - Telegram"[^>]*data-state="unchecked"|data-state="unchecked"[^>]*aria-label="Mood reminder - Telegram"/,
);
});
});
+9 -8
View File
@@ -12,10 +12,6 @@ import { EmptyState } from "@/components/ui/empty-state";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import Link from "next/link";
import { Bell, Settings, AlertCircle } from "lucide-react";
import {
EVENT_DEFAULT_ENABLED,
type EventType,
} from "@/lib/notifications/types";
import {
eventDescriptionTranslationKey,
eventTranslationKey,
@@ -48,6 +44,11 @@ interface PreferencesData {
channels: ChannelInfo[];
preferences: Preference[];
eventTypes: string[];
/**
* What each event does on a channel with no stored row, resolved by the
* server for this account with the function the dispatcher uses.
*/
eventDefaults: Record<string, boolean>;
}
export default function NotificationsPage() {
@@ -116,10 +117,10 @@ export default function NotificationsPage() {
(p) => p.channelId === channelId && p.eventType === eventType,
);
if (pref) return pref.enabled;
// v1.4.25 — per-event default. Most events stay opt-out
// (no row = ON); PERSONAL_RECORD flips to opt-in (no row = OFF)
// so a backfill doesn't fire hundreds of pushes on first sync.
return EVENT_DEFAULT_ENABLED[eventType as EventType] ?? true;
// No row: the server's resolved default for this account. Read, never
// recomputed here: the mood reminder's follows the reminder card, and a
// copy of the constant table showed it off while it was being delivered.
return data?.eventDefaults?.[eventType] === true;
}
// Cross-links to the other two notification surfaces (reminder types +