mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(notifications): strengthen the Web Push path for PWA-only self-hosters
Self-hosters without an Apple Developer account rely on Web Push to the installed PWA — there is no Live Activity or lock-screen widget to drive. Make that path carry the dose lifecycle on its own: - Stable per-slot tag. The medication dose-due reminder now sets a `med:<id>:<iso-slot>` Web Push tag (medicationDoseTag), so a re-fired reminder for the same slot coalesces instead of stacking, and a later clear can target it. - Clear-on-taken. When a dose is marked taken or skipped, push a `type:"clear"` notification reusing that slot tag. The service worker closes the matching pending reminder and refreshes the badge — the PWA equivalent of ending a Live Activity. A snooze leaves the dose outstanding and clears nothing. Wired into the single + bulk intake routes alongside the existing APNs cross-device sync. - App badge. The dispatcher carries a server-authoritative outstanding- dose count; the SW reflects it via the Badging API (setAppBadge/clearAppBadge), feature-detected and a no-op where unsupported. - The SW push handler honours requireInteraction for urgent events and renotify so a re-fired reminder re-alerts. Dose-due, preventive-care, and low-stock reminders already cascade to Web Push when no APNs channel exists; tests pin that an APNs-less instance still delivers.
This commit is contained in:
1 parent
0163f159d5
commit
604aec631f
11 files changed
+666
-10
No files matched your search
@@ -69,6 +69,8 @@ import {
|
||||
} from "@/lib/validations/medication";
|
||||
import type { InjectionSiteKey } from "@/lib/medications/injection-sites";
|
||||
import { dispatchMedicationIntakeSyncBulk } from "@/lib/notifications/medication-intake-sync";
|
||||
import { dispatchMedicationIntakeWebClearBulk } from "@/lib/notifications/web-push-clear";
|
||||
import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses";
|
||||
|
||||
const MAX_ENTRIES_PER_BATCH = 500;
|
||||
const BATCH_RATE_LIMIT_MAX = 60;
|
||||
@@ -711,6 +713,23 @@ async function postBulk(request: NextRequest): Promise<Response> {
|
||||
slots: Array.from(syncSlots.values()),
|
||||
originDeviceToken: request.headers.get("x-device-id"),
|
||||
});
|
||||
|
||||
// v1.18.4 — PWA-only counterpart: close the still-pending dose-due
|
||||
// reminders for every affected slot over Web Push and refresh the app
|
||||
// badge once with the post-batch outstanding-dose count. Best-effort,
|
||||
// fire-and-forget — a clear miss never affects the batch response.
|
||||
const affectedSlots = Array.from(syncSlots.values());
|
||||
void (async () => {
|
||||
const badgeCount = await countOutstandingDosesToday(
|
||||
user.id,
|
||||
user.timezone,
|
||||
);
|
||||
await dispatchMedicationIntakeWebClearBulk({
|
||||
userId: user.id,
|
||||
slots: affectedSlots,
|
||||
badgeCount,
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
return apiSuccess({
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
type InjectionSiteValue,
|
||||
} from "@/lib/validations/medication";
|
||||
import { dispatchMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync";
|
||||
import { dispatchMedicationIntakeWebClear } from "@/lib/notifications/web-push-clear";
|
||||
import { countOutstandingDosesToday } from "@/lib/medications/outstanding-doses";
|
||||
|
||||
const querySchema = z.object({
|
||||
scope: z.enum(["today", "compliance"]),
|
||||
@@ -698,5 +700,23 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
originDeviceToken: request.headers.get("x-device-id"),
|
||||
});
|
||||
|
||||
// v1.18.4 — PWA-only equivalent of the Live Activity end: when a dose is
|
||||
// resolved (taken / skipped), push a `type:"clear"` to the user's Web Push
|
||||
// subscriptions so the still-pending dose-due reminder for this slot is
|
||||
// closed by the service worker (matched on the stable slot tag) and the app
|
||||
// badge re-reflects the outstanding-dose count. A snooze leaves the dose
|
||||
// outstanding, so it neither clears the reminder nor changes the badge.
|
||||
if (status === "taken" || status === "skipped") {
|
||||
void (async () => {
|
||||
const badgeCount = await countOutstandingDosesToday(user.id, userTzForHook);
|
||||
await dispatchMedicationIntakeWebClear({
|
||||
userId: user.id,
|
||||
medicationId: existing.medicationId,
|
||||
scheduledFor: (movedTo ?? existing.scheduledFor).toISOString(),
|
||||
badgeCount,
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
return apiSuccess(updated);
|
||||
});
|
||||
Reference in new issue
Block a user