Mood-reminder hotfix bundle on top of v1.4.38.1 plus the missing Settings toggle. A six-axis review of the v0.5.4 iOS-coordination patch surfaced enough real blockers that the mood-reminder feature could not have been used safely as shipped.
Added
- Settings toggle for the daily mood reminder — new card under
/settings/notificationswith a single Switch wired to the user profile flag. Six-locale copy (de/en/es/fr/it/pl) for the title, description, status text, and toasts. Without this surface the feature shipped in v1.4.38.1 was unreachable for end users. DispatchOutcomereturn type ondispatchNotificationso callers can decide whether the send is committed to their own ledger. Existing callers ignore the return and keep working — the function is still best-effort and never throws.localHmAsUtchelper returns the UTC instant of localhh:mmon the local calendar day implied bynow. Re-derives the offset at the target local time so the result stays correct on DST transition days.- Daily 03:25 Europe/Berlin retention sweep for the
mood_reminder_dispatchesledger: deletes rows older than 90 days.
Fixed
- Mood-reminder ledger writes only after the dispatcher confirms delivery. A
dispatched = falseoutcome (no channel succeeded) leaves the slot empty so the next tick is free to retry once the user adds a channel or upstream recovers. - Mood-reminder locale resolver honours every supported locale. v1.4.38.1 shipped translations for es/fr/it/pl but the resolver silently demoted those users to English copy.
- Mood-reminder FR body restores the apostrophe in
aujourd'hui. v1.4.38.1 shipped without it; VoiceOver reads the lockscreen body aloud, so the typo was audible. - Medication-reminder
scheduledForand the iOS-snoozescheduledAtISO are now DST-safe. The previous arithmetic added raw UTC hours to local midnight and drifted by an hour on spring-forward / fall-back days. - Per-user
trywrapper around the mood-reminder tick. A single bad row (corrupt timezone, dispatcher exception) used to abort the whole 22:00 candidate pass; now the wide-event records a per-user failure counter and the rest of the cohort is unaffected. - APNs payload whitelists iOS-relevant metadata keys. The dispatcher's
metadatafield is shared across every channel and was forwarding channel-specific payload structures (Telegram'sreplyMarkupinline-keyboard object, ad-hoc extras) into the APNsuserInfoApple sees plus the iOS notification handler. Allowlist now covers only what iOS actually reads.
Refactor
- Dead
MoodReminderCandidateinterface dropped + redundant select field removed (thewhereclause already filters to opted-in users). - CHANGELOG entry for v1.4.38.1 rewritten to drop the
EVENT_DEFAULT_ENABLEDidentifier reference and describe the default-off posture in user-readable language.
Tests
- Unit suite 4565 → 4551 (rewrote
mood-reminder.test.tsfor the new contract; net delta is a rewrite, not a coverage loss). Added tests for ledger-after-delivery semantics, per-user try wrapper, P2002-race semantics, six-locale dispatch, and the FR apostrophe regression.
Operator notes
- No new migration. No env-var change.
- Coolify auto-deploys main on tag push; first webhook may pull stale
:latest, redeploy after the docker-publish workflow completes.