Skip to content

Notifications chiclet (persisted + synced) + archive-on-import - #50

Closed
mayeack wants to merge 4 commits into
mainfrom
claude/notifications-and-archive-suggestions
Closed

mayeack wants to merge 4 commits into
mainfrom
claude/notifications-and-archive-suggestions

Conversation

@mayeack

@mayeack mayeack commented Jun 4, 2026

Copy link
Copy Markdown
Owner

The final two requested features (B3 + B4). Stacked on #49 (which is stacked on #48); bases retarget as each merges.

New synced model: AppNotification

id, kind, title, message, contactId, isRead, isDismissed + the standard sync/provenance fields. Wired end-to-end like the other synced models: client + server schema, ModelSyncApply.appNotificationToDict / applyRemoteAppNotification (same conflict resolution), LocalServerSyncService push/pull/flip, and BackupServer push/pull/deletes.

Data safety: the schema addition is additive — no currentSchemaVersion bump (that path wipes the store). AppNotification has no relationship to Contact (scalar contactId), so SwiftData performs a purely additive lightweight migration; existing contacts/interactions are preserved.

Generation (NotificationService, deduped one-per-contact-per-kind, idempotent)

  • Fading relationships: contacts whose score slipped below the fading threshold but is still > 0 — mirrors the Dashboard "Fading Relationships" rule, so the ~1000 never-engaged score-0 contacts don't flood the list (capped at 10 new/run, most-faded first). Generated on every sync pull on the background context, right after the score recalc, so it reflects fresh scores regardless of the visible tab.
  • Archive suggestions (B4): ContactSyncService.mergeOrInsert, on a full Import All only, flags active contacts whose cnContactIdentifier is no longer in the live address-book set.

UI

A bell chiclet with an active-count badge in the Overview toolbar. Tapping opens a sheet listing non-dismissed notifications newest-first: tap or swipe-leading → open the contact (dedicated navigationDestination), swipe-trailing → dismiss, and archive suggestions get an inline Archive action (sets isHidden). DashboardView keeps its manual-fetch (no @Query) crash-safe pattern.

Tests

NotificationsTests — fading generation scope/idempotency/dismissed-not-recreated, archive dedup, and the AppNotification sync round-trip + conflict resolution. Full macOS suite green: 232 tests, 0 failures. iOS + macOS app + BlackbookServer all build.

⚠️ Required after merge

BlackbookServer is not on TestFlight — to relay notifications cross-device, rebuild + reinstall /Applications/BlackbookServer.app from a Release build and relaunch (it briefly restarts the always-on backup server). The additive schema migration runs on first launch.

Note on cross-device archive suggestions: they're derived from this device's address book, so a contact missing on the Mac but present on the iPhone could surface an archive suggestion on the iPhone — dismissable; acceptable for v1.

🤖 Generated with Claude Code

Michael Yeack and others added 4 commits June 3, 2026 17:53
…he score

Two app-breaking issues, shipped together as an urgent hotfix.

1. Import-All crash / app won't reopen
   ContactSyncService imported on the MAIN ModelContext and saved once there.
   "Import All" inserts hundreds of Contacts; that main-context save fires
   ContactListView's @query mid-transaction and faults the Contact.interactions
   inverse (EXC_BAD_ACCESS) — the exact failure PR #43 fixed for the pull path.
   Because startAutoSync re-imports on every .onAppear, it became a crash loop
   so the app couldn't reopen.
   Fix: importContacts/importSelected apply mergeOrInsert on a background
   ModelContext (autosaveEnabled=false) with a single settled save, so the main
   context's @queries only ever see one committed state. startAutoSync also
   defers the launch import by 1s so the first frame renders first. Posts
   .blackbookSyncDidComplete after import so the Dashboard refetches.

2. Recent interactions don't raise the relationship score (Hugo Dooner)
   The score reads the denormalized Contact.lastInteractionDate. Synced
   iMessage interaction *records* arrive cleanly, but the contact-field update
   carrying lastInteractionDate is rejected by conflict resolution when the
   local copy is newer + pending — so recency stays 0 and the score sits at
   priority-only (exactly 20 / "Fading").
   Fix: RelationshipScoreEngine.recalculateAll now re-derives each contact's
   lastInteractionDate from the Interaction records this device already holds
   (single FetchDescriptor<Interaction>, grouped by the to-one
   interaction.contact?.id — the same controlled pattern the server uses; never
   touches the faulting Contact.interactions inverse), taking max() so a newer
   manual date is never lowered. recalculateAll is now also invoked right after
   each sync pull on the background context, so scores refresh regardless of the
   visible tab (on macOS the Dashboard's own recalc wouldn't run otherwise).

Tests: +3 regression cases in RelationshipScoreEngineTests covering the
heal-from-records behavior (stale date healed, priority contact recovers above
the boost, newer manual date preserved). Full macOS suite green (220 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…en-filter chokepoint

Three of the five requested features (notifications + archive-on-import follow in PR 3).

2a. Three suggested records in contact pickers
New ContactSuggestionEngine ranks candidates by contextual similarity to the subject
— shared tags (x3), groups (x2), locations (x2), plus a per-field signal — and falls
back to relationship score so suggestions are always available. IntroducedToPickerView
and MetViaPickerView now show a "Suggested" section (top 3) when not searching, with the
rest under "All Contacts".

2b. Click column headers to sort
ContactListViewModel gains sortColumn + sortAscending and toggleSort(): clicking a
header sorts by that column; clicking the active column flips direction. The active
column shows a chevron. Sorts Name, Score, Groups, Locations, Tags, Met via, Introduced
to (string columns sort their first value alphabetically with blanks last; numeric
columns default to descending). The sort menu was updated to the same model and keeps
Recent/Added. Replaces the old ContactSortOrder enum.

2e. Hidden contacts excluded from every search surface
Audit found all selection surfaces already filtered !isHidden && !isMergedAway. Added a
single chokepoint — `Sequence<Contact>.selectable` — and adopted it in the contact
pickers and ContactListViewModel so the CLAUDE.md rule has one enforcement point.
(A hidden contact still appearing on another device is sync propagation, not a missing
filter — same conflict-resolution family as the score fix in PR #48.)

Tests: ContactSuggestionEngineTests (ranking, exclusions, score fallback) and
ContactListViewModelTests (+toggle direction, string-column blanks-last, .selectable).
Full macOS suite green (226 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tions

Implements the Overview Notifications chiclet and the import archive-suggestion
(items B3 + B4), the final two requested features.

New synced model: AppNotification (id, kind, title, message, contactId, isRead,
isDismissed + the standard sync/provenance fields). Wired end-to-end exactly like
the other synced models:
- client + server schema (additive — no schema-version bump, so existing data is
  preserved via SwiftData lightweight migration; AppNotification has no relationship
  to Contact, only a scalar contactId, so the migration is purely additive)
- ModelSyncApply.appNotificationToDict / applyRemoteAppNotification (conflict
  resolution identical to the other models)
- LocalServerSyncService push (count/body/mark-synced) + pull + bootstrap flip list
- BackupServer handleSyncPull / handleSyncPush / applySyncDeletes

Generation (NotificationService, deduped one-per-contact-per-kind, idempotent):
- Fading relationships: contacts whose score slipped below the fading threshold but
  is still > 0 (mirrors the Dashboard "Fading Relationships" definition, so the
  ~1000 never-engaged score-0 contacts don't flood the list; capped at 10 new/run).
  Generated on every sync pull on the background context, right after the score
  recalc, so it reflects fresh scores regardless of the visible tab.
- Archive suggestions (B4): ContactSyncService.mergeOrInsert, on a full "Import All"
  only, flags active contacts whose cnContactIdentifier is no longer in the live
  address-book set.

UI: a bell chiclet with an active-count badge in the Overview toolbar. Tapping opens
a sheet (NotificationsView) listing non-dismissed notifications newest-first; tap or
swipe-leading opens the related contact (via a dedicated navigationDestination), swipe-
trailing dismisses, and archive suggestions get an inline Archive action (sets isHidden).
DashboardView keeps its manual-fetch pattern (no @query) to stay crash-safe.

Tests: NotificationsTests — fading generation scope/idempotency/dismissed-not-recreated,
archive dedup, and the AppNotification sync round-trip + conflict resolution. Full macOS
suite green (232 tests). iOS + macOS app + BlackbookServer all build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mayeack
mayeack changed the base branch from claude/features-suggestions-sort-notifications to main June 4, 2026 03:10
mayeack added a commit that referenced this pull request Jun 4, 2026
…tions (#52)

Implements the Overview Notifications chiclet (B3) and import archive suggestions (B4).
(Rebuilt cleanly on main after the stacked base branch was deleted; supersedes #50.)

New synced model AppNotification (id, kind, title, message, contactId, isRead,
isDismissed + sync/provenance). Additive migration — no schema-version bump (which
would wipe the store); AppNotification has no Contact relationship (scalar contactId),
so SwiftData migrates additively and existing data is preserved. Wired end-to-end like
the other synced models: ModelSyncApply (toDict/applyRemote), client LocalServerSyncService
push/pull/flip, server BackupServer pull/push/applySyncDeletes, both schemas + TestHelpers.

Generation (NotificationService, deduped one-per-contact-per-kind, idempotent):
- Fading: contacts with 0 < score < fading threshold (mirrors the Dashboard fading
  definition; capped 10/run), generated each sync pull after the score recalc.
- Archive (B4): ContactSyncService.mergeOrInsert, full Import All only, flags active
  contacts whose cnContactIdentifier left the live address-book set.

UI: bell chiclet + count badge in the Overview toolbar -> sheet NotificationsView
(swipe-leading open / trailing dismiss / inline Archive). DashboardView keeps its
manual-fetch (no @query) crash-safe pattern.

Tests: NotificationsTests (generation, dedup, sync round-trip + conflict). Full macOS
suite green (232 tests). macOS app + BlackbookServer build clean.

Co-authored-by: Michael Yeack <mayeack@Michaels-Mac-mini.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@mayeack

mayeack commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #52 (rebuilt on main after the stacked base branch was deleted on #48's merge).

@mayeack mayeack closed this Jun 4, 2026
@mayeack
mayeack deleted the claude/notifications-and-archive-suggestions branch June 4, 2026 03:16
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