mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(notifications): personal-records push opt-in toggle (default OFF)
Adds the PERSONAL_RECORD event type to the dispatcher vocabulary and flips its default policy to opt-in. Without an explicit preference row the dispatcher now suppresses PR pushes — so a multi-year Apple Health backfill writes the records but does not saturate the lock-screen on first sync. Other events keep the opt-out default they always had. The /notifications matrix renders the new event automatically because it iterates EVENT_TYPES; the per-event default helper keeps the toggle in the right initial state across all six locales (EN, DE, FR, ES, IT, PL).
This commit is contained in:
1 parent
7b7a896c44
commit
15a72bb329
10 files changed
+313
-14
No files matched your search
+31
-1
@@ -583,6 +583,34 @@
|
||||
"glp1Inventory": "{pens} Pens übrig · ~{weeks} Wochen Reichweite",
|
||||
"glp1InventoryLow": "Niedriger Bestand — bald nachbestellen",
|
||||
"glp1LogSideEffect": "Nebenwirkung loggen",
|
||||
"inventory": {
|
||||
"title": "Bestand",
|
||||
"addPen": "Neuen Pen erfassen",
|
||||
"addPenTitle": "Neuen Pen erfassen",
|
||||
"loading": "Bestand wird geladen…",
|
||||
"empty": "Noch keine Pens erfasst. Erfasse den nächsten, um die 30-Tage-Frist zu starten.",
|
||||
"dosesLabel": "Dosen",
|
||||
"printedExpiry": "Verfallsdatum auf der Schachtel: {date}",
|
||||
"purchasedAt": "Gekauft am {date}",
|
||||
"pastTitle": "Frühere Pens ({count})",
|
||||
"markAsInUse": "Als angebrochen markieren",
|
||||
"markAsUsedUp": "Als verbraucht markieren",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"save": "Speichern",
|
||||
"fieldDosesTotal": "Dosen pro Pen",
|
||||
"fieldDosesTotalHelp": "Anzahl der Dosen, die dieser Pen enthält. Mounjaro-KwikPen = 4, Trulicity Single-Dose = 1, Byetta = 60.",
|
||||
"fieldPrintedExpiry": "Aufgedrucktes Verfallsdatum",
|
||||
"fieldPurchasedAt": "Kaufdatum",
|
||||
"fieldNotes": "Notiz (optional)",
|
||||
"errorDosesTotal": "Dosen pro Pen müssen zwischen 1 und 100 liegen",
|
||||
"state": {
|
||||
"active": "Im Kühlschrank",
|
||||
"inUse": "In Verwendung · {days} Tage übrig",
|
||||
"expired": "Abgelaufen",
|
||||
"usedUp": "Verbraucht"
|
||||
}
|
||||
},
|
||||
"injectionSiteBodyOutlineAriaLabel": "Körperumriss",
|
||||
"injectionSitePickerAriaLabel": "Injektionsstelle auswählen",
|
||||
"siteAbdomenLeft": "Bauch, unten links",
|
||||
@@ -2792,7 +2820,9 @@
|
||||
"eventWithingsSyncFailed": "Withings-Sync-Fehler",
|
||||
"eventWithingsSyncFailedDesc": "Meldung bei Fehlern beim automatischen Withings-Datenabgleich.",
|
||||
"eventSystemAlert": "System-Hinweise",
|
||||
"eventSystemAlertDesc": "Wichtige Systemnachrichten und administrative Hinweise."
|
||||
"eventSystemAlertDesc": "Wichtige Systemnachrichten und administrative Hinweise.",
|
||||
"eventPersonalRecord": "Persönlicher Rekord",
|
||||
"eventPersonalRecordDesc": "Push bei einem neuen Bestwert für eine erfasste Kennzahl oder ein Training. Standardmäßig aus."
|
||||
},
|
||||
"format": {
|
||||
"dateShort": "{day}.{month}.{year}",
|
||||
|
||||
+31
-1
@@ -583,6 +583,34 @@
|
||||
"glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply",
|
||||
"glp1InventoryLow": "Low stock — refill soon",
|
||||
"glp1LogSideEffect": "Log side effect",
|
||||
"inventory": {
|
||||
"title": "Inventory",
|
||||
"addPen": "Add new pen",
|
||||
"addPenTitle": "Register new pen",
|
||||
"loading": "Loading inventory…",
|
||||
"empty": "No pens tracked yet. Add the next one to start the 30-day in-use clock.",
|
||||
"dosesLabel": "doses",
|
||||
"printedExpiry": "Carton expiry {date}",
|
||||
"purchasedAt": "Bought {date}",
|
||||
"pastTitle": "Past pens ({count})",
|
||||
"markAsInUse": "Mark in use",
|
||||
"markAsUsedUp": "Mark used up",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"fieldDosesTotal": "Doses per pen",
|
||||
"fieldDosesTotalHelp": "Number of doses this pen ships with. Mounjaro KwikPen = 4, Trulicity single-dose = 1, Byetta = 60.",
|
||||
"fieldPrintedExpiry": "Printed expiry date",
|
||||
"fieldPurchasedAt": "Date purchased",
|
||||
"fieldNotes": "Notes (optional)",
|
||||
"errorDosesTotal": "Doses per pen must be between 1 and 100",
|
||||
"state": {
|
||||
"active": "Refrigerated",
|
||||
"inUse": "In use · {days} days left",
|
||||
"expired": "Expired",
|
||||
"usedUp": "Used up"
|
||||
}
|
||||
},
|
||||
"injectionSiteBodyOutlineAriaLabel": "Body outline",
|
||||
"injectionSitePickerAriaLabel": "Injection site picker",
|
||||
"siteAbdomenLeft": "Abdomen, lower left",
|
||||
@@ -2792,7 +2820,9 @@
|
||||
"eventWithingsSyncFailed": "Withings Sync Failed",
|
||||
"eventWithingsSyncFailedDesc": "Alert when the automatic Withings data sync fails.",
|
||||
"eventSystemAlert": "System Alert",
|
||||
"eventSystemAlertDesc": "Important system messages and administrative notices."
|
||||
"eventSystemAlertDesc": "Important system messages and administrative notices.",
|
||||
"eventPersonalRecord": "Personal Record",
|
||||
"eventPersonalRecordDesc": "Push when an all-time best is reached for a tracked metric or workout. Off by default."
|
||||
},
|
||||
"format": {
|
||||
"dateShort": "{month}/{day}/{year}",
|
||||
|
||||
+32
-2
@@ -583,6 +583,34 @@
|
||||
"glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply",
|
||||
"glp1InventoryLow": "Low stock — refill soon",
|
||||
"glp1LogSideEffect": "Log side effect",
|
||||
"inventory": {
|
||||
"title": "Inventario",
|
||||
"addPen": "Añadir nueva pluma",
|
||||
"addPenTitle": "Registrar nueva pluma",
|
||||
"loading": "Cargando inventario…",
|
||||
"empty": "Aún no hay plumas registradas. Añade la siguiente para iniciar el contador de 30 días.",
|
||||
"dosesLabel": "dosis",
|
||||
"printedExpiry": "Caducidad de la caja: {date}",
|
||||
"purchasedAt": "Comprado el {date}",
|
||||
"pastTitle": "Plumas anteriores ({count})",
|
||||
"markAsInUse": "Marcar como abierta",
|
||||
"markAsUsedUp": "Marcar como agotada",
|
||||
"delete": "Eliminar",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"fieldDosesTotal": "Dosis por pluma",
|
||||
"fieldDosesTotalHelp": "Número de dosis que contiene la pluma. KwikPen de Mounjaro = 4, Trulicity dosis única = 1, Byetta = 60.",
|
||||
"fieldPrintedExpiry": "Fecha de caducidad impresa",
|
||||
"fieldPurchasedAt": "Fecha de compra",
|
||||
"fieldNotes": "Nota (opcional)",
|
||||
"errorDosesTotal": "Las dosis por pluma deben estar entre 1 y 100",
|
||||
"state": {
|
||||
"active": "Refrigerada",
|
||||
"inUse": "En uso · {days} días restantes",
|
||||
"expired": "Caducada",
|
||||
"usedUp": "Agotada"
|
||||
}
|
||||
},
|
||||
"injectionSiteBodyOutlineAriaLabel": "Silueta del cuerpo",
|
||||
"injectionSitePickerAriaLabel": "Selector de sitio de inyección",
|
||||
"siteAbdomenLeft": "Abdomen, lower left",
|
||||
@@ -2791,8 +2819,10 @@
|
||||
"eventComplianceLowDesc": "Notification when medication compliance drops below a threshold.",
|
||||
"eventWithingsSyncFailed": "Withings Sync Failed",
|
||||
"eventWithingsSyncFailedDesc": "Alert when the automatic Withings data sync fails.",
|
||||
"eventSystemAlert": "System Alert",
|
||||
"eventSystemAlertDesc": "Important system messages and administrative notices."
|
||||
"eventSystemAlert": "Alerta del sistema",
|
||||
"eventSystemAlertDesc": "Mensajes importantes del sistema y avisos administrativos.",
|
||||
"eventPersonalRecord": "Récord personal",
|
||||
"eventPersonalRecordDesc": "Notificación cuando se alcanza un nuevo récord en una métrica o entrenamiento. Desactivado por defecto."
|
||||
},
|
||||
"format": {
|
||||
"dateShort": "{month}/{day}/{year}",
|
||||
|
||||
+32
-2
@@ -583,6 +583,34 @@
|
||||
"glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply",
|
||||
"glp1InventoryLow": "Low stock — refill soon",
|
||||
"glp1LogSideEffect": "Log side effect",
|
||||
"inventory": {
|
||||
"title": "Inventaire",
|
||||
"addPen": "Ajouter un nouveau stylo",
|
||||
"addPenTitle": "Enregistrer un nouveau stylo",
|
||||
"loading": "Chargement de l'inventaire…",
|
||||
"empty": "Aucun stylo suivi. Ajoutez le prochain pour démarrer le compte à rebours de 30 jours.",
|
||||
"dosesLabel": "doses",
|
||||
"printedExpiry": "Péremption sur la boîte : {date}",
|
||||
"purchasedAt": "Acheté le {date}",
|
||||
"pastTitle": "Anciens stylos ({count})",
|
||||
"markAsInUse": "Marquer comme entamé",
|
||||
"markAsUsedUp": "Marquer comme épuisé",
|
||||
"delete": "Supprimer",
|
||||
"cancel": "Annuler",
|
||||
"save": "Enregistrer",
|
||||
"fieldDosesTotal": "Doses par stylo",
|
||||
"fieldDosesTotalHelp": "Nombre de doses contenues dans ce stylo. KwikPen Mounjaro = 4, Trulicity dose unique = 1, Byetta = 60.",
|
||||
"fieldPrintedExpiry": "Date de péremption imprimée",
|
||||
"fieldPurchasedAt": "Date d'achat",
|
||||
"fieldNotes": "Note (facultatif)",
|
||||
"errorDosesTotal": "Le nombre de doses par stylo doit être compris entre 1 et 100",
|
||||
"state": {
|
||||
"active": "Au réfrigérateur",
|
||||
"inUse": "En cours · {days} jours restants",
|
||||
"expired": "Périmé",
|
||||
"usedUp": "Épuisé"
|
||||
}
|
||||
},
|
||||
"injectionSiteBodyOutlineAriaLabel": "Silhouette du corps",
|
||||
"injectionSitePickerAriaLabel": "Sélecteur de site d'injection",
|
||||
"siteAbdomenLeft": "Abdomen, lower left",
|
||||
@@ -2791,8 +2819,10 @@
|
||||
"eventComplianceLowDesc": "Notification when medication compliance drops below a threshold.",
|
||||
"eventWithingsSyncFailed": "Withings Sync Failed",
|
||||
"eventWithingsSyncFailedDesc": "Alert when the automatic Withings data sync fails.",
|
||||
"eventSystemAlert": "System Alert",
|
||||
"eventSystemAlertDesc": "Important system messages and administrative notices."
|
||||
"eventSystemAlert": "Alerte système",
|
||||
"eventSystemAlertDesc": "Messages système importants et avis administratifs.",
|
||||
"eventPersonalRecord": "Record personnel",
|
||||
"eventPersonalRecordDesc": "Notification lorsqu'un nouveau record est atteint pour une mesure ou un entraînement. Désactivé par défaut."
|
||||
},
|
||||
"format": {
|
||||
"dateShort": "{month}/{day}/{year}",
|
||||
|
||||
+32
-2
@@ -583,6 +583,34 @@
|
||||
"glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply",
|
||||
"glp1InventoryLow": "Low stock — refill soon",
|
||||
"glp1LogSideEffect": "Log side effect",
|
||||
"inventory": {
|
||||
"title": "Inventario",
|
||||
"addPen": "Aggiungi nuova penna",
|
||||
"addPenTitle": "Registra nuova penna",
|
||||
"loading": "Caricamento inventario…",
|
||||
"empty": "Nessuna penna registrata. Aggiungi la prossima per avviare il conto alla rovescia di 30 giorni.",
|
||||
"dosesLabel": "dosi",
|
||||
"printedExpiry": "Scadenza sulla scatola: {date}",
|
||||
"purchasedAt": "Acquistata il {date}",
|
||||
"pastTitle": "Penne precedenti ({count})",
|
||||
"markAsInUse": "Segna come in uso",
|
||||
"markAsUsedUp": "Segna come esaurita",
|
||||
"delete": "Elimina",
|
||||
"cancel": "Annulla",
|
||||
"save": "Salva",
|
||||
"fieldDosesTotal": "Dosi per penna",
|
||||
"fieldDosesTotalHelp": "Numero di dosi contenute nella penna. KwikPen Mounjaro = 4, Trulicity monodose = 1, Byetta = 60.",
|
||||
"fieldPrintedExpiry": "Data di scadenza stampata",
|
||||
"fieldPurchasedAt": "Data di acquisto",
|
||||
"fieldNotes": "Nota (facoltativo)",
|
||||
"errorDosesTotal": "Le dosi per penna devono essere comprese tra 1 e 100",
|
||||
"state": {
|
||||
"active": "In frigorifero",
|
||||
"inUse": "In uso · {days} giorni rimasti",
|
||||
"expired": "Scaduta",
|
||||
"usedUp": "Esaurita"
|
||||
}
|
||||
},
|
||||
"injectionSiteBodyOutlineAriaLabel": "Sagoma del corpo",
|
||||
"injectionSitePickerAriaLabel": "Selettore del sito di iniezione",
|
||||
"siteAbdomenLeft": "Abdomen, lower left",
|
||||
@@ -2791,8 +2819,10 @@
|
||||
"eventComplianceLowDesc": "Notification when medication compliance drops below a threshold.",
|
||||
"eventWithingsSyncFailed": "Withings Sync Failed",
|
||||
"eventWithingsSyncFailedDesc": "Alert when the automatic Withings data sync fails.",
|
||||
"eventSystemAlert": "System Alert",
|
||||
"eventSystemAlertDesc": "Important system messages and administrative notices."
|
||||
"eventSystemAlert": "Avviso di sistema",
|
||||
"eventSystemAlertDesc": "Messaggi di sistema importanti e avvisi amministrativi.",
|
||||
"eventPersonalRecord": "Record personale",
|
||||
"eventPersonalRecordDesc": "Notifica quando viene raggiunto un nuovo record per una metrica o un allenamento. Disattivato per impostazione predefinita."
|
||||
},
|
||||
"format": {
|
||||
"dateShort": "{month}/{day}/{year}",
|
||||
|
||||
+32
-2
@@ -583,6 +583,34 @@
|
||||
"glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply",
|
||||
"glp1InventoryLow": "Low stock — refill soon",
|
||||
"glp1LogSideEffect": "Log side effect",
|
||||
"inventory": {
|
||||
"title": "Magazyn",
|
||||
"addPen": "Dodaj nowy wstrzykiwacz",
|
||||
"addPenTitle": "Zarejestruj nowy wstrzykiwacz",
|
||||
"loading": "Ładowanie magazynu…",
|
||||
"empty": "Brak wstrzykiwaczy w magazynie. Dodaj następny, aby uruchomić 30-dniowy licznik.",
|
||||
"dosesLabel": "dawki",
|
||||
"printedExpiry": "Data ważności na opakowaniu: {date}",
|
||||
"purchasedAt": "Zakupiono {date}",
|
||||
"pastTitle": "Wcześniejsze wstrzykiwacze ({count})",
|
||||
"markAsInUse": "Oznacz jako otwarty",
|
||||
"markAsUsedUp": "Oznacz jako zużyty",
|
||||
"delete": "Usuń",
|
||||
"cancel": "Anuluj",
|
||||
"save": "Zapisz",
|
||||
"fieldDosesTotal": "Dawki na wstrzykiwacz",
|
||||
"fieldDosesTotalHelp": "Liczba dawek w jednym wstrzykiwaczu. Mounjaro KwikPen = 4, Trulicity jednodawkowy = 1, Byetta = 60.",
|
||||
"fieldPrintedExpiry": "Nadrukowana data ważności",
|
||||
"fieldPurchasedAt": "Data zakupu",
|
||||
"fieldNotes": "Notatka (opcjonalnie)",
|
||||
"errorDosesTotal": "Liczba dawek na wstrzykiwacz musi mieścić się w zakresie 1–100",
|
||||
"state": {
|
||||
"active": "W lodówce",
|
||||
"inUse": "W użyciu · pozostało {days} dni",
|
||||
"expired": "Wygasł",
|
||||
"usedUp": "Zużyty"
|
||||
}
|
||||
},
|
||||
"injectionSiteBodyOutlineAriaLabel": "Sylwetka ciała",
|
||||
"injectionSitePickerAriaLabel": "Wybór miejsca iniekcji",
|
||||
"siteAbdomenLeft": "Abdomen, lower left",
|
||||
@@ -2791,8 +2819,10 @@
|
||||
"eventComplianceLowDesc": "Notification when medication compliance drops below a threshold.",
|
||||
"eventWithingsSyncFailed": "Withings Sync Failed",
|
||||
"eventWithingsSyncFailedDesc": "Alert when the automatic Withings data sync fails.",
|
||||
"eventSystemAlert": "System Alert",
|
||||
"eventSystemAlertDesc": "Important system messages and administrative notices."
|
||||
"eventSystemAlert": "Alert systemowy",
|
||||
"eventSystemAlertDesc": "Ważne komunikaty systemowe i powiadomienia administracyjne.",
|
||||
"eventPersonalRecord": "Rekord osobisty",
|
||||
"eventPersonalRecordDesc": "Powiadomienie po osiągnięciu nowego rekordu w metryce lub treningu. Domyślnie wyłączone."
|
||||
},
|
||||
"format": {
|
||||
"dateShort": "{month}/{day}/{year}",
|
||||
|
||||
@@ -8,6 +8,10 @@ import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { Bell, Loader2, Settings, AlertCircle } from "lucide-react";
|
||||
import {
|
||||
EVENT_DEFAULT_ENABLED,
|
||||
type EventType,
|
||||
} from "@/lib/notifications/types";
|
||||
|
||||
interface ChannelInfo {
|
||||
id: string;
|
||||
@@ -115,8 +119,11 @@ export default function NotificationsPage() {
|
||||
const pref = data?.preferences?.find(
|
||||
(p) => p.channelId === channelId && p.eventType === eventType,
|
||||
);
|
||||
// Opt-out model: default is ON when no preference row exists
|
||||
return pref ? pref.enabled : true;
|
||||
if (pref) return pref.enabled;
|
||||
// v1.4.25 W16c — 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;
|
||||
}
|
||||
|
||||
if (authLoading || isLoading) {
|
||||
|
||||
@@ -309,6 +309,80 @@ describe("dispatchNotification — cooldown skip", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("dispatchNotification — per-event default policy (v1.4.25 W16c)", () => {
|
||||
it("PERSONAL_RECORD defaults OFF when no preference row exists", async () => {
|
||||
const channel = makeChannel({
|
||||
type: "NTFY",
|
||||
config: JSON.stringify({
|
||||
serverUrl: "https://ntfy.example",
|
||||
topic: "t",
|
||||
}),
|
||||
preferences: [],
|
||||
});
|
||||
vi.mocked(prisma.notificationChannel.findMany).mockResolvedValueOnce([
|
||||
channel,
|
||||
] as never);
|
||||
|
||||
await dispatchNotification({
|
||||
eventType: "PERSONAL_RECORD",
|
||||
userId: "u-1",
|
||||
title: "PR",
|
||||
message: "Best ever",
|
||||
});
|
||||
|
||||
// No row → opt-in for this event type → sender NOT called.
|
||||
expect(sendViaNtfyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("PERSONAL_RECORD fires when the user has explicitly opted in", async () => {
|
||||
const channel = makeChannel({
|
||||
type: "NTFY",
|
||||
config: JSON.stringify({
|
||||
serverUrl: "https://ntfy.example",
|
||||
topic: "t",
|
||||
}),
|
||||
preferences: [{ eventType: "PERSONAL_RECORD", enabled: true }],
|
||||
});
|
||||
vi.mocked(prisma.notificationChannel.findMany).mockResolvedValueOnce([
|
||||
channel,
|
||||
] as never);
|
||||
sendViaNtfyMock.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
await dispatchNotification({
|
||||
eventType: "PERSONAL_RECORD",
|
||||
userId: "u-1",
|
||||
title: "PR",
|
||||
message: "Best ever",
|
||||
});
|
||||
|
||||
expect(sendViaNtfyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("legacy events (MEDICATION_REMINDER) still default ON when no row exists", async () => {
|
||||
const channel = makeChannel({
|
||||
type: "NTFY",
|
||||
config: JSON.stringify({
|
||||
serverUrl: "https://ntfy.example",
|
||||
topic: "t",
|
||||
}),
|
||||
preferences: [],
|
||||
});
|
||||
vi.mocked(prisma.notificationChannel.findMany).mockResolvedValueOnce([
|
||||
channel,
|
||||
] as never);
|
||||
sendViaNtfyMock.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
await dispatchNotification({
|
||||
eventType: "MEDICATION_REMINDER",
|
||||
userId: "u-1",
|
||||
title: "Reminder",
|
||||
message: "Take it",
|
||||
});
|
||||
|
||||
expect(sendViaNtfyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dispatchNotification — Telegram hard reject", () => {
|
||||
it("auto-disables on 'chat not found'", async () => {
|
||||
const channel = makeChannel({
|
||||
|
||||
@@ -5,7 +5,9 @@ import type {
|
||||
TelegramChannelConfig,
|
||||
NtfyChannelConfig,
|
||||
ChannelType,
|
||||
EventType,
|
||||
} from "@/lib/notifications/types";
|
||||
import { EVENT_DEFAULT_ENABLED } from "@/lib/notifications/types";
|
||||
import { sendViaTelegram } from "@/lib/notifications/senders/telegram";
|
||||
import { sendViaNtfy } from "@/lib/notifications/senders/ntfy";
|
||||
import { sendViaWebPush } from "@/lib/notifications/senders/web-push";
|
||||
@@ -113,10 +115,21 @@ export async function dispatchNotification(
|
||||
channels.sort((a, b) => channelPriority(a.type) - channelPriority(b.type));
|
||||
|
||||
const now = new Date();
|
||||
// v1.4.25 W16c — per-event default policy. Most events stay
|
||||
// opt-out (no row = enabled); PERSONAL_RECORD flips to opt-in
|
||||
// (no row = disabled) so the iOS backfill doesn't saturate the
|
||||
// lock-screen on first launch. The user enables it explicitly
|
||||
// from /settings/notifications once they've seen the badge a few
|
||||
// times and decided they want push.
|
||||
const defaultEnabled =
|
||||
EVENT_DEFAULT_ENABLED[payload.eventType as EventType] ?? true;
|
||||
for (const channel of channels) {
|
||||
// Opt-out model: if no preference row exists, default to enabled
|
||||
const pref = channel.preferences[0];
|
||||
if (pref && !pref.enabled) continue;
|
||||
if (pref) {
|
||||
if (!pref.enabled) continue;
|
||||
} else if (!defaultEnabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Backoff cooldown — skip the channel until `nextRetryAt`. Without
|
||||
// this guard, a flapping upstream would burn API quota every
|
||||
|
||||
@@ -13,6 +13,10 @@ export const EVENT_TYPES = [
|
||||
"COMPLIANCE_LOW",
|
||||
"WITHINGS_SYNC_FAILED",
|
||||
"SYSTEM_ALERT",
|
||||
// v1.4.25 W16c — PR detection. Default OFF (see DEFAULT_EVENT_PREFS
|
||||
// below) so a multi-year Apple Health backfill doesn't fire hundreds
|
||||
// of pushes during initial sync.
|
||||
"PERSONAL_RECORD",
|
||||
] as const;
|
||||
export type EventType = (typeof EVENT_TYPES)[number];
|
||||
|
||||
@@ -22,6 +26,27 @@ export const EVENT_TYPE_LABELS: Record<EventType, string> = {
|
||||
COMPLIANCE_LOW: "Low compliance",
|
||||
WITHINGS_SYNC_FAILED: "Withings sync failed",
|
||||
SYSTEM_ALERT: "System alerts",
|
||||
PERSONAL_RECORD: "Personal records",
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-event default policy when no `NotificationPreference` row
|
||||
* exists. The dispatcher reads this to decide whether to fire on a
|
||||
* channel that has never been explicitly toggled for the event.
|
||||
*
|
||||
* Every event defaults to ON (the original opt-out model) EXCEPT
|
||||
* `PERSONAL_RECORD`: PRs are silent by default and require the user
|
||||
* to explicitly enable them in `/settings/notifications`. Rationale —
|
||||
* a fresh iOS install backfilling years of HealthKit history would
|
||||
* otherwise saturate the user's lock-screen on first launch.
|
||||
*/
|
||||
export const EVENT_DEFAULT_ENABLED: Record<EventType, boolean> = {
|
||||
MEDICATION_REMINDER: true,
|
||||
MEASUREMENT_ANOMALY: true,
|
||||
COMPLIANCE_LOW: true,
|
||||
WITHINGS_SYNC_FAILED: true,
|
||||
SYSTEM_ALERT: true,
|
||||
PERSONAL_RECORD: false,
|
||||
};
|
||||
|
||||
export const CHANNEL_TYPE_LABELS: Record<ChannelType, string> = {
|
||||
|
||||
Reference in new issue
Block a user