Skip to content

feat(notify): desktop notifications — the final Phase-2 item - #37

Merged
Harikeshav-R merged 6 commits into
mainfrom
feat/desktop-notifications
Aug 6, 2026
Merged

feat(notify): desktop notifications — the final Phase-2 item#37
Harikeshav-R merged 6 commits into
mainfrom
feat/desktop-notifications

Conversation

@Harikeshav-R

Copy link
Copy Markdown
Owner

What & why

Adds desktop notifications (PROJECT.md §5.16) — the last unchecked Phase-2 item. The
daemon now posts native OS notifications for new high-fit matches and upcoming
deadlines
even when the TUI is closed, so the user is alerted out-of-app. With this, Phase
2 (Discovery & background) is complete.

It follows the established platform-seam pattern (atlas.platform): a Notifier Protocol, a
sys.platform-independent pragma'd backend over desktop-notifier
(D-Bus / Notification Center / WinRT), and a best-effort / no-op contract so a dead backend
never breaks a poll. Ships disabled by default.

Key commits

  1. build(deps) — add desktop-notifier (ships py.typed, no mypy override).
  2. feat(notify) — the Notifier platform seam (atlas.platform.notifier) + FakeNotifier.
  3. feat(notify)[notifications] config + best-effort notify_best_effort + pure
    quiet-hours/daily-cap gating.
  4. feat(notify) — the service + JSON run-state (mirroring the probe cache) + two new queries
    (list_new_high_fit, upcoming_deadlines).
  5. feat(notify) — fire notifications after scoring from both daemon poll paths (the
    scheduled tick and the on-demand IPC poll).
  6. docs — record the feature; mark Phase 2 complete (STATUS.md, PROJECT.md §15, CHANGELOG,
    README).

Design notes

  • Dedup without a schema change: the polls only return counts, so "new since last tick"
    uses a persisted JSON run-state (a monotonic score-id high-water mark for matches; stable
    per-deadline keys for deadlines). No Alembic migration.
  • Deadlines live only inside the application.status_history JSON (StatusTransition.due),
    so upcoming_deadlines scans it for non-terminal applications rather than querying a column.
  • macOS caveat (§5.16): only a signed interpreter can post notifications; the best-effort
    fallback covers the unsigned case (documented in the README).
  • Triggers deferred to Phase 3 (they depend on Phase-3 features): the "ready-but-not-applied"
    nudge, no-response follow-up, and email-scan status changes; plus actionable click callbacks.

Testing

  • 1206 tests at 100% line + branch coverage; only the pragma'd default_notifier body is
    excluded (posts a real OS notification).
  • ruff format --check + ruff check + mypy --strict (incl. --platform win32) all clean;
    uv build confirms the wheel still builds.
  • Hermetic throughout: a FakeNotifier, db_engine, and a fixed clock drive the flow — a
    fresh high-fit score fires, a re-poll suppresses (high-water mark), quiet hours + daily cap
    are respected, a status_history due becomes a deadline notification, and a raising
    notifier is swallowed (poll/IPC still succeed).

Refs PROJECT.md §5.16, §15 (Phase 2).

Adds the desktop-notifier dependency (PyPI, MIT) — one cross-platform API
over the native notification backends (D-Bus on Linux, Notification Center
on macOS, WinRT on Windows) — for the daemon's desktop-notification feature
(PROJECT.md §5.16). The package ships a py.typed marker, so no mypy
ignore-missing-imports override is needed.
Add the desktop-notification boundary as the next piece of the platform
abstraction (PROJECT.md §12.1), mirroring the file-open and URL-open seams:
a secret-free `NotifyError`, a `@runtime_checkable` `Notifier` protocol
(a `(title, message)` callable), and `default_notifier` — a pragma'd
production impl over desktop-notifier's `DesktopNotifierSync`, which
dispatches to the native backend (D-Bus / Notification Center / WinRT) and
degrades gracefully. The heavy import is lazy so the hermetic suite never
loads it, and any backend failure (no D-Bus, an unsigned macOS interpreter,
…) maps to the one domain error for the best-effort caller to swallow.

Re-export the three names from the package and add a `FakeNotifier` (the
opener-fake mold: records `(title, message)`, optionally raises) plus the
three canonical seam tests. The daemon will fire these for new high-fit
matches and upcoming deadlines (PROJECT.md §4.1, §5.16).
Model the [notifications] section (PROJECT.md §5.16, §10): a
NotificationsConfig with all fields defaulted — enabled (ships disabled,
like [aggregators]), min_match_score (80), deadline_lead_hours (24),
quiet_hours ("22:00-08:00"), daily_cap (20) — nested on Config and no
longer ignored by the schema.

Add the atlas.notify package scaffolding it needs:
- emit.notify_best_effort: fire-and-forget dispatch mirroring the daemon's
  emit_progress seam — no-ops on a None notifier and logs-and-swallows any
  failure, so a dead backend (no D-Bus, an unsigned macOS interpreter, …)
  never breaks a poll.
- window: pure quiet-hours parsing (a "HH:MM-HH:MM" window that wraps past
  midnight; a malformed/empty spec disables quiet hours rather than
  crashing) and a per-calendar-day key for the daily cap.

The service and state that consume these land next.
Add the pieces the daemon needs to decide *what* to notify about and *once*:

- state: a NotifyState persisted as JSON under the state dir (a new
  notify_state_file path helper), mirroring the probe-cache idiom — a
  missing/corrupt file yields fresh default state (never a crash). It holds
  the high-water mark of the last match score notified, the per-day count
  for the cap, and the deadline keys already alerted.
- matching.repository.list_new_high_fit: the profile's postings scored at or
  above min_score whose (append-only, monotonic) score id is past the
  high-water mark — exactly the matches new since the last notification.
- tracking.repository.upcoming_deadlines: deadlines live only inside the
  status_history JSON, so a pure scanner over non-terminal applications
  collects each StatusTransition.due within [now, now + lead_hours),
  skipping a malformed entry rather than crashing, with a stable per-deadline
  key so each fires once.
- service.notify_after_poll: the after-poll orchestrator — short-circuits
  when disabled or inside quiet hours, notifies new high-fit matches across
  every profile (one shared baseline so interleaved score ids are not
  skipped) then upcoming deadlines, posting best-effort and advancing the
  run-state, all bounded by the daily cap. Every boundary injected (notifier,
  clock, state) so the suite drives it with a FakeNotifier and a fixed clock.
Wire the notification pass into the daemon after scoring, at both entry
points that run the three-poll pass:

- Add run_after_poll_notifications — the daemon-facing wrapper that loads
  the run-state, opens its own session (so the just-committed scores are
  visible), runs notify_after_poll, and saves the advanced state. It
  short-circuits before any disk I/O when notifications are disabled (the
  common case) and is wholly best-effort — any internal failure is logged
  and swallowed, returning a no-op outcome, so a notification problem never
  fails the poll.
- The scheduled tick (atlas daemon start's bound run()) calls it after the
  scoring poll.
- The on-demand IPC poll (daemon.ipc._run_poll) calls it after scoring too;
  handle_request/_run_poll gain an injected notifier param (default the real
  one) so the suite drives it with a FakeNotifier. A raising notifier still
  yields the terminal ResultEvent, never an ErrorEvent.
Desktop notifications are the last Phase-2 item, so Phase 2 (Discovery &
background) is now complete:

- STATUS.md: new 'What has landed' entry, Phase-2 row → complete in the
  at-a-glance table, and the 'Next up' pointer moved to Phase 3 (CalDAV,
  IMAP email scan, Q&A drafting).
- PROJECT.md §15: a checked Desktop-notifications item under Phase 2 and the
  IPC item's 'Remaining Phase-2 work' note dropped.
- CHANGELOG.md: an Unreleased/Added entry (+ the desktop-notifier dep).
- README.md: a 'Desktop notifications' section (the [notifications] config,
  the ships-disabled default, graceful degradation, and the macOS
  signed-interpreter caveat), plus the daemon section refreshed to mention
  aggregators, per-profile scoring, notifications, and 'atlas daemon poll'.
@Harikeshav-R
Harikeshav-R merged commit b516dc1 into main Aug 6, 2026
10 checks passed
@Harikeshav-R
Harikeshav-R deleted the feat/desktop-notifications branch August 6, 2026 08:40
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.

1 participant