mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
chore(release): v1.4.38.2
Mood-reminder hotfix bundle on top of v1.4.38.1 plus the missing Settings toggle. Six-axis review of the v0.5.4 iOS-coord patch surfaced enough blockers that the feature could not have been used safely as shipped: - Locale resolver demoted 4 of 6 locales (es/fr/it/pl) to English. - Dedup ledger committed before delivery — a transient APNs blip silently nuked the day's nudge and blocked retries. - DST-unsafe arithmetic drifted the local-22:00 baseline by an hour twice a year for both the mood-reminder push and the medication iOS-snooze action. - APNs payload forwarded the Telegram `replyMarkup` inline-keyboard object and other channel-specific extras to Apple's servers and the iOS notification handler. - No Settings UI for the per-user opt-in flag — the feature was unreachable for end users regardless of the other fixes. v1.4.38.2 closes every item above plus adds the missing `MoodReminderCard` Switch, a 90-day retention sweep for the dispatch ledger, and a per-user `try` wrapper so one bad row cannot abort the 22:00 tick. See CHANGELOG.md.
This commit is contained in:
1 parent
c4f2e0bc51
commit
d224811a34
27 files changed
+2165
-206
No files matched your search
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { SmilePlus } from "lucide-react";
|
||||
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
interface ProfileShape {
|
||||
moodReminderEnabled: boolean;
|
||||
}
|
||||
|
||||
export function MoodReminderCard({
|
||||
isAuthenticated,
|
||||
}: {
|
||||
isAuthenticated: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const [optimistic, setOptimistic] = useState<boolean | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [msgType, setMsgType] = useState<"success" | "error" | null>(null);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ["user", "profile"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/user/profile");
|
||||
if (!res.ok) throw new Error("Failed to load profile");
|
||||
return (await res.json()).data as ProfileShape;
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const enabled = optimistic ?? profile?.moodReminderEnabled ?? false;
|
||||
|
||||
async function handleToggle(next: boolean) {
|
||||
setOptimistic(next);
|
||||
setSaving(true);
|
||||
setMsg(null);
|
||||
setMsgType(null);
|
||||
|
||||
const res = await fetch("/api/user/profile", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ moodReminderEnabled: next }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setMsg(
|
||||
next
|
||||
? t("notifications.moodReminder.enabledToast")
|
||||
: t("notifications.moodReminder.disabledToast"),
|
||||
);
|
||||
setMsgType("success");
|
||||
await queryClient.invalidateQueries({ queryKey: ["user", "profile"] });
|
||||
setOptimistic(null);
|
||||
} else {
|
||||
setOptimistic(null);
|
||||
setMsg(t("notifications.moodReminder.saveError"));
|
||||
setMsgType("error");
|
||||
}
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-mood-reminder-title"
|
||||
className="bg-card rounded-lg border p-4 sm:p-6"
|
||||
>
|
||||
<SettingsCardHeader
|
||||
icon={SmilePlus}
|
||||
title={t("notifications.moodReminder.title")}
|
||||
titleId="settings-mood-reminder-title"
|
||||
description={t("notifications.moodReminder.description")}
|
||||
status={
|
||||
<label className="flex min-h-11 items-center gap-3">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={!isAuthenticated || saving}
|
||||
aria-label={t("notifications.moodReminder.toggleAria")}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{enabled
|
||||
? t("notifications.moodReminder.statusOn")
|
||||
: t("notifications.moodReminder.statusOff")}
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
/>
|
||||
{msg && (
|
||||
<p
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className={
|
||||
msgType === "error"
|
||||
? "text-destructive mt-3 text-sm"
|
||||
: "text-muted-foreground mt-3 text-sm"
|
||||
}
|
||||
>
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { ChevronRight } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { MoodReminderCard } from "@/components/settings/mood-reminder-card";
|
||||
import { NotificationStatusCard } from "@/components/settings/notification-status-card";
|
||||
import { NtfyCard } from "@/components/settings/ntfy-card";
|
||||
import { TelegramCard } from "@/components/settings/telegram-card";
|
||||
@@ -105,6 +106,9 @@ export function NotificationsSection() {
|
||||
<WebPushCard />
|
||||
</div>
|
||||
)}
|
||||
<div id="mood-reminder" className="scroll-mt-28">
|
||||
<MoodReminderCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user