Skip to content

feat(notifications): add self-hosted offline reminder engine - #5

Open
mtherien wants to merge 5 commits into
mainfrom
claude/dual-mode-notifications-srqn23
Open

feat(notifications): add self-hosted offline reminder engine#5
mtherien wants to merge 5 commits into
mainfrom
claude/dual-mode-notifications-srqn23

Conversation

@mtherien

Copy link
Copy Markdown
Contributor

No description provided.

claude added 5 commits July 13, 2026 22:34
Adds "download once, alert anytime" local notifications so self-hosted
deployments (which have no push transport and may be intranet-only) still
alert the device. Cloud APNs/FCM push is unchanged.

Server (Web.Shared + Core + Infrastructure):
- IUpcomingReminderService / UpcomingReminderService: projects domain state
  forward into future-dated reminders (calendar reminders at
  StartTime-ReminderMinutesBefore; per-item expiry; low-stock and task
  digests at a local morning hour; date-anchored chore/vehicle-maintenance
  due dates). Reuses the existing evaluators' query predicates and respects
  each user's per-type PushEnabled preference.
- GET /api/v1/notifications/upcoming?days=N (per-authenticated-user).
- Unit tests for FireAtUtc math, preference gating, and ordering.

Mobile (MAUI):
- INotificationScheduler with iOS (UNCalendarNotificationTrigger) and
  Android (AlarmManager.SetExactAndAllowWhileIdle + boot re-arm) impls.
- NotificationSyncOrchestrator mirrors CalendarSyncOrchestrator: gated on
  ApiSettings.IsSelfHostedServer(), diffs the server feed against a SQLite
  mirror (schedule new/changed, cancel stale, prune past), caps at 60.
- Background drivers (iOS BGTaskScheduler, Android WorkManager) plus a
  foreground/resume trigger; settings toggle shown only in self-hosted mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Chb8Xj6dRF1fiPZc6nqqE
…e cache

Make the on-device local scheduler the single path that DISPLAYS scheduled
reminders (calendar/expiry/low-stock/task) in both cloud and self-hosted, and
demote cloud push for those types to a silent data-sync channel. Removes
double-delivery by construction and gives cloud users real-time freshness.

- MessageType.IsLocallySchedulable() classifies the four scheduled types.
- IReminderSyncPushService (+ NullReminderSyncPushService no-op default) mirrors
  the contact-sync silent-push pattern; CalendarController fires it on event
  create/update/delete (cloud sends a silent reminderSync push; self-hosted no-op).
- Client handles action=reminderSync (iOS AppDelegate, Android FCM service) by
  re-running NotificationSyncOrchestrator.SyncAsync(); the orchestrator now runs
  in both modes (dropped the self-hosted gate) and defaults on in cloud; the
  Settings toggle shows in both modes.
- Unit test for the classifier.

No schema change. Cloud-side visible-push suppression + the cloud silent-push
sender live in the private repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Chb8Xj6dRF1fiPZc6nqqE
…ent)

Adds IAppBadgeService (iOS sets the springboard badge via SetBadgeCount /
ApplicationIconBadgeNumber; Android no-op — launcher-managed) and an
AppBadgeHelper that sets the badge = server unread-notification count.

Wired to run on the existing AppShell unread poll, on app resume, and after
the user reads/dismisses notifications (the reset). The server also stamps the
same count onto outgoing pushes (private repo) so the badge updates while the
app is closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Chb8Xj6dRF1fiPZc6nqqE
…r-item

UpcomingReminderService now emits one aggregate Expiry reminder ("N item(s)
expiring soon", with expired/expiring-soon breakdown) fired at the next local
morning, instead of one local notification per stock entry. Mirrors the server
ExpiryEvaluator's digest and the existing LowStock/TaskSummary digests, so the
device shows a single expiry banner per day rather than one per item. Updated
unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Chb8Xj6dRF1fiPZc6nqqE
When the user dismisses a Famick OS notification, mark the corresponding in-app
notification read so the unread count and app-icon badge stay in sync.

Server/shared: CreateNotificationAsync returns the new notification id;
InAppMessageTransport stamps it onto RenderedMessage.NotificationId (runs before
the push transport) so the push can carry it.

Client:
- Android: visible pushes are now data-only, so FamickFirebaseMessagingService
  always posts the notification itself with a delete intent ->
  NotificationDismissReceiver marks it read (reliable in fore/background).
- iOS: registers a FAMICK_DISMISSIBLE category with a custom dismiss action;
  ForegroundNotificationDelegate handles the dismiss action and marks read
  (best-effort — iOS only reports explicit dismissals).
- Shared NotificationActionHelper marks read + refreshes the badge.

Cloud-side push payload changes (aps.category + notificationId; FCM data-only)
land in the private repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Chb8Xj6dRF1fiPZc6nqqE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants