feat: push de agenda sin OSINT (compone persona enlazada) + sync inverso por etag

Privacidad (decisión del usuario: al móvil solo datos de agenda):
- _compose_agenda_vcard compone el vCard desde el contacto (fn/tels/emails) + las
  direcciones (ADR) y aliases (NICKNAME) de la persona enlazada por note_path, SIN pasar
  nunca el dict osint a build_vcard → el vCard jamás lleva X-OSINT-* (DNI/sexo/fecha-nac
  quedan solo en DuckDB+Obsidian). Usado en upsert_contact y en el push masivo push_all_dav
  (que antes leía solo contacts y perdía las direcciones).

Sync inverso DAVx5→DuckDB (last-write-wins por etag):
- Tras cada push se captura el etag nuevo del recurso (dav_list_resources) y se persiste en
  contacts.etag, para no confundir el push propio con una edición del móvil.
- POST /api/sync/dav-pull: pull incremental — compara etags, descarga SOLO los recursos
  cambiados/nuevos (dav_get_resource + parse_vcard + upsert), borra los que el móvil quitó,
  re-enlaza. Distinto del ingest_dav (DELETE+INSERT ciego): respeta la verdad de la DB salvo
  donde el etag prueba un cambio externo.

20 tests verdes (18 + 2 nuevos: vCard sin OSINT con direcciones; pull incremental por etag).
This commit is contained in:
2026-06-13 10:53:23 +02:00
parent d672f4f73e
commit b620cc38c2
5 changed files with 465 additions and 15 deletions
+6
View File
@@ -93,6 +93,10 @@ dav_list_addressbooks = _load_registry_fn(
"infra", "dav_list_addressbooks", "dav_list_addressbooks"
)
dav_collection_ctag = _load_registry_fn("infra", "dav_collection_ctag", "dav_collection_ctag")
# Grupo dav: listado incremental (PROPFIND Depth:1 -> [{href, etag}]) y GET de
# un recurso suelto. Base del sync inverso por etag (/api/sync/dav-pull).
dav_list_resources = _load_registry_fn("infra", "dav_list_resources", "dav_list_resources")
dav_get_resource = _load_registry_fn("infra", "dav_get_resource", "dav_get_resource")
# Grupo dav: escritura (push DB -> Xandikos) y creación de colecciones.
carddav_put_vcard = _load_registry_fn("infra", "carddav_put_vcard", "carddav_put_vcard")
@@ -137,6 +141,8 @@ __all__ = [
"dav_get_collection",
"dav_list_calendars",
"dav_list_addressbooks",
"dav_list_resources",
"dav_get_resource",
"carddav_put_vcard",
"caldav_put_event",
"dav_delete_resource",