Files
HealthLog/src/lib/medications/outstanding-doses.ts
T
Marc-André Bombeck 604aec631f 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.
2026-06-18 11:39:55 +02:00

40 lines
1.3 KiB
TypeScript

/**
* v1.18.4 — server-authoritative count of a user's still-outstanding doses
* for the current local day. Drives the PWA app badge (`navigator.setAppBadge`)
* so the installed icon mirrors how many doses still need logging — the
* free-tier equivalent of the iOS widget's pending count.
*
* Outstanding = a today-scheduled `MedicationIntakeEvent` that is neither
* taken (`takenAt == null`) nor skipped (`skipped == false`) nor swept by the
* miss cutoff (`autoMissed == false`). Pending rows are minted by
* `projectTodayIntakesAndRecompute`; this is a read-only count over the same
* window, so callers that have just projected today's rows get an accurate
* total.
*/
import { prisma } from "@/lib/db";
import { getUserTodayBounds } from "@/lib/tz/local-day";
/**
* Count the user's outstanding (pending) doses for today in their timezone.
* Best-effort: any failure returns 0 so a badge update never blocks a caller.
*/
export async function countOutstandingDosesToday(
userId: string,
tz: string,
): Promise<number> {
try {
const { start, end } = getUserTodayBounds(new Date(), tz);
return await prisma.medicationIntakeEvent.count({
where: {
userId,
scheduledFor: { gte: start, lte: end },
takenAt: null,
skipped: false,
autoMissed: false,
},
});
} catch {
return 0;
}
}