Promote dev -> main: M1 Calendar + M2 Contacts verticals #220

Merged
KofTwentyTwo merged 6 commits from dev into main 2026-07-29 23:33:54 +00:00
6 Commits
Author SHA1 Message Date
james.maesandJames Maes 174c5457f4 feat(contacts): add Contacts gRPC vertical (proto, daemon, CLI, E2E) (#218)
Publish the M2-S1 contacts engine over the daemon's authenticated gRPC
API, mirroring the Calendar vertical: add nuncio.v1.Contacts (Sync,
ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/
ContactPhone messages to the proto contract and regenerate the golden
descriptor.

Wire ContactsEngineOverrides + ContactsGrpcService into nunciod,
mounted behind its own BearerAuthInterceptor exactly like every other
service. ListContacts/GetContact always read genuinely persisted data
from DatabaseEngine; Sync fetches from an injected ContactsBackend
(MockContactsBackend in tests) and persists via
contacts_sync::sync_with_backend, mirroring calendar_sync.rs; with no
backend injected, production Sync returns an honest error since
AccountConfig carries no per-account CardDAV configuration yet.

Add connect_contacts/AuthenticatedContactsClient to nuncio-proto's
shared client helpers. Replace the CLI's Commands::Contact handlers
(previously a disposable in-memory ContactsDatabase.in_memory() per
invocation) with real gRPC calls against the daemon, so a contact
added via `contact add` persists and is visible to a later, separate
CLI invocation.

Deviations from the issue's literal proto sketch, flagged for review:
- Added CreateContact (not in the issue's message list): the
  acceptance criteria requires `contact add` to persist a locally-
  authored contact across CLI invocations, but the daemon has no
  per-account CardDAV credential to write back to a real server. This
  RPC only ever writes to the daemon's own local store -- never a
  fabricated CardDAV write-back -- which is the same honest-surface
  spirit as Calendar's "no create/delete" note (that note is about not
  pretending to write to the remote server, not about never
  persisting locally-authored data at all).
- ListContacts/CreateContact take a required account_id (the issue
  sketched a bare ListContactsRequest{}), because the real
  DatabaseEngine::list_contacts/save_contact are account-scoped;
  ContactSubcommand::List/Search/Add/Sync now all take --account to
  match.
- Contact search has no server-side RPC; `contact search` fetches the
  full ListContacts result and filters client-side by display name,
  organization, and email.
- Contact's created_at/updated_at/last_interacted_at are omitted from
  the proto message -- no client use case identified for them yet.

Full-daemon offline E2E (contacts_e2e_test.rs) proves Sync -> List/Get
over the authenticated API, CreateContact's persistence surviving a
second, independent client connection, and an unauthenticated call
rejected with Code::Unauthenticated.

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-29 18:23:32 -05:00
james.maesandJames Maes 41665e85d5 feat(contacts): build a real CardDAV engine, replacing the fabricated client (#217)
Replace CardDavClient::fetch_remote_vcards's hardcoded "Google CardDAV
Sync Contact" return with a real reqwest REPORT/addressbook-query
client that parses a genuine multistatus response. Add a real vCard
(RFC 6350) parser -- the crate previously only had a generator -- and
prove round-trip fidelity through Contact::to_vcard(). Add the
ContactsBackend trait and MockContactsBackend, mirroring
nuncio_cal::CalendarBackend/MockCalendarBackend. Distinguish transport
failures from parse failures via CardDavError::TransportFailed/
ParseFailed rather than ever silently succeeding with fabricated data.

Give nuncio-store real, tested save_contact/list_contacts/get_contact
CRUD backed by a genuine SQLite table + trigram FTS mirror, so contacts
persist through the daemon's own store instead of nuncio-contacts's
disconnected ContactsDatabase pool. This takes nuncio-store as a
narrow, deliberate dependency on nuncio-contacts::Contact.

Opt nuncio-contacts into the workspace lint gate ([lints] workspace,
#![forbid(unsafe_code)]) for parity with every other crate.

Wiremock-only, offline: no live network calls in tests.

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-29 17:53:55 -05:00
james.maesandJames Maes 9496d63eaa M1: Wire RRULE recurrence expansion into calendar event queries (#215)
* feat(calendar): expand RRULE recurrence in ListEvents and filter by calendar_id

Wire the previously-dead nuncio_cal::RecurrenceEngine into
CalendarGrpcService::list_events: non-recurring events pass through
unchanged, while recurring masters are expanded into real occurrences
for the requested window via a new store method
(list_recurring_calendar_events, no time filter since a master's own
timestamps can precede the window while its occurrences fall inside
it) and a pure expand_events_for_window merge helper that keeps
recurring rows sourced only from expansion, never double-counting a
master that also overlaps the window in the raw query.

Fix expand_occurrences to stop trusting an empty match as "return the
raw master": that fabricated an out-of-window result whenever a
window fell entirely outside a bounded (UNTIL/COUNT) series, and could
hang scanning an unbounded series for a match that would never come.
skip_while/take_while over the RRuleSet's monotonic iterator bounds
the walk and returns a genuinely empty result instead.

Also apply the calendar_id filter list_events accepted but ignored,
closing the gap between it and the mock backend's own filtering.

Refs #186

* docs(nunciod): reword stale comment referencing removed rrule fallback

---------

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-29 17:35:44 -05:00
james.maesandJames Maes f926d09839 feat(calendar): add Calendar gRPC vertical (proto, daemon, CLI, E2E) (#214)
Wire the orphaned nuncio-cal engine into nunciod as a real, authenticated
gRPC vertical mirroring Mail: add service Calendar (Sync/ListEvents/GetEvent)
to nuncio.proto with a regenerated descriptor golden, connect_calendar in
nuncio-proto's client, calendar_sync::sync_with_backend plus
CalendarEngineOverrides/CalendarGrpcService mounted behind its own
BearerAuthInterceptor in nunciod::grpc, and real `cal sync`/`cal list` gRPC
clients in nuncio-cli replacing the fabricated CLI output. ListEvents/GetEvent
always read genuinely persisted data; production Sync returns an honest error
since there is no persisted per-account CalDAV configuration yet, and only an
injected MockCalendarBackend exercises a working Sync. Adds unit, daemon
integration, and offline full-daemon E2E tests, including an
unauthenticated-call-rejected assertion for the new service.

Refs #185

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-29 17:19:15 -05:00
James Maes 5b4365a28b docs: correct stale CI-paused note; ignore stray trivy scan logs 2026-07-29 16:52:37 -05:00
dependabot[bot]andjames.maes b551716190 deps(cargo): bump icalendar from 0.15.8 to 0.17.13 (#210)
Bumps [icalendar](https://github.com/hoodie/icalendar) from 0.15.8 to 0.17.13.
- [Release notes](https://github.com/hoodie/icalendar/releases)
- [Changelog](https://github.com/hoodie/icalendar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hoodie/icalendar/compare/v0.15.8...v0.17.13)

---
updated-dependencies:
- dependency-name: icalendar
  dependency-version: 0.17.13
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: James Maes <james@kof22.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 16:49:34 -05:00