Files
email-assistant/openspec/changes/contacts-caldav-server/tasks.md
T
hermes 8ea022f5c0 CardDAV: двусторонний синк контактов с Radicale (push+pull) + docs DAVx5/Caddy
- scripts/contacts_caldav_sync.py: push 81 контакт → vCard (Radicale :5232),
  pull правок/создания/удаления с телефона → contacts.json (ETag сверка,
  soft-delete, конфликты 412 → приоритет телефону, лог caldav-sync.log)
- fix(contacts_extractor): перестройка by_email-индексов после sort()
- STATUS.md: раздел DAVx5 + Caddy (Authorization/handle_path/X-Script-Name),
  Задача 3 (синхронизация с телефоном) = следующая сессия
- tasks.md: отмечены 1.x-3.x/V (реализовано и протестировано), добавлен
  раздел 5 (Caddy + DAVx5 + jtx board) для следующей сессии
2026-09-13 17:21:51 +00:00

8.5 KiB
Raw Blame History

Tasks: CardDAV-сервер для синхронизации контактов

1. Адресная книга в Radicale

  • 1.1 Создать адресную книгу «Контакты» на ФС: mkdir -p /opt/hermes/email-assistant/radicale/data/collections/collection-root/estorozhenko/Контакты и проверить, что Radicale видит её как addressbook: curl -u estorozhenko:$RADICALE_PASS -X PROPFIND -H 'Depth: 0' http://127.0.0.1:5232/estorozhenko/Контакты/ → HTTP 207 и в XML есть <C:addressbook> (если RS не определяется — добавить .Radicale.props с addressbook и повторить). Сделано: MKCOL через протокол дал 201; .Radicale.props = {"tag": "VADDRESSBOOK"}.
  • 1.2 Положить тестовую vCard (test.vcf) в коллекцию и проверить, что она отдаётся: curl ... /estorozhenko/Контакты/test.vcf → 200 + vCard; затем удалить тестовую карточку.

2. Синк в contacts_extractor.py

  • 2.1 Рефакторинг: вынести генерацию vCard 4.0 из существующей generate_vcard() в отдельную функцию contact_to_vcard(contact) -> str, чтобы переиспользовать для CardDAV-карточек. Проверка: скрипт запускается без ошибок, contacts.vcf генерируется как раньше. Сделано: в scripts/contacts_caldav_sync.py (модульная выноска, contacts_extractor не тронут).
  • 2.2 Добавить функцию sync_contacts_to_caldav(contacts_dir, base_url, user, password, prune=False): читает contacts.json, строит карту uid→(ETag, href) через PROPFIND Depth:1, PUT создаёт/обновляет vCard, при prune=True DELETE удаляет лишние. Использовать urllib + Basic Auth. Проверка: юнит-запуск с тестовым Radicale (см. 3.x).
  • 2.3 CLI-флаги: --sync-caldav, --prune-caldav, --caldav-url, --caldav-user, --caldav-pass (или env CALDAV_PASS). По умолчанию синк выключен. Проверка: python3 scripts/contacts_extractor.py --help показывает все флаги; без --sync-caldav поведение прежнее. Сделано: CLI в contacts_caldav_sync.py (--sync-caldav выключен по умолчанию, --dry-run и пр.)
  • 2.4 Двусторонняя сверка: sync_carddav() после push выполняет pull — PROPFIND Depth:1, сравнение ETag, создание/обновление контактов из новых/изменённых карточек (REQ-008/009), deleted: true при удалении карточки (REQ-010). Проверка: см. 3.5-3.8 (reverse-pull тесты).
  • 2.5 ETag-конфликты: при PUT с If-Match и ответе 412 — принять версию с сервера, локальную записать в caldav-sync.log (conflict_local), продолжить (REQ-011). Проверка: ручной тест — изменить vCard на сервере И контакт в базе, запустить синк, увидеть conflict в логе.

3. Сквозной тест sync

  • 3.1 Запустить синк с реальной базой: python3 scripts/contacts_extractor.py --sync-caldav (или отдельный скрипт) → в Radicale появились карточки (счётчик PROPFIND/cards): curl ... PROPFIND Depth:1 /estorozhenko/Контакты/ показывает N карточек ≈ количеству контактов в contacts.json с email. Сделано: 81 карточка создана (81 контакт), PROPFIND 207.
  • 3.2 Повторный запуск — количество карточек не растёт (идемпотентность). Сделано: стабильно 162 unchanged (81 push + 81 pull), 0 PUT.
  • 3.3 Изменить контакт в contacts.json (добавить телефон) → повторный синк обновляет карточку (TEL появился, ETag изменился).
  • 3.4 --prune-caldav: удалить контакт из contacts.json → карточка удалена с сервера.
  • 3.5 Reverse pull: вручную (curl PUT) изменить vCard на сервере → повторный синк обновляет контакт в contacts.json. Сделано: добавлен/удалён TEL на сервере → база обновилась/обнулила phone.
  • 3.6 Новая карточка на сервере: curl PUT новой vCard → синк создаёт контакт в базе. Сделано: pulled_created (id=phone-newcard-001, from_device=True).
  • 3.7 Удаление на сервере: curl DELETE vCard → контакт получает deleted: true в базе. Сделано: pulled_deleted, артефакт убран из базы.
  • 3.8 Конфликт: изменить и vCard (curl), и контакт в базе → синк применяет версию с сервера, локальная в caldav-sync.log.

4. Интеграция в cron и документация

  • 4.1 Обновить config/contacts-cron.sh: добавить --sync-caldav и подтянуть пароль из radicale/.env (env CALDAV_PASS). Проверка: запуск cron-скрипта вручную синкает контакты без ошибок.
  • 4.2 Обновить README/STATUS: раздел «CardDAV (контакты)» — как подключить на Android (DAVx5, URL http://cal.nixg.ru:5232 или cal.nixg.ru, логин estorozhenko), порты, флаги синка. Сделано (2026-09-13): STATUS.md — новый раздел «DAVx⁵ (Android: CalDAV/CardDAV-мост)» + Caddy-конфиги (Authorization, handle_path, X-Script-Name), Задача 3 помечена как следующая сессия.
  • 4.3 openspec validate contacts-caldav-server → valid.
  • 4.4 Git commit и push (gitea.nixg.ru/hermes/email-assistant).

5. Синхронизация с телефоном (СЛЕДУЮЩАЯ СЕССИЯ — первая задача)

  • 5.1 Настроить Caddy на vps02: cal.nixg.ru → Radicale bigbox :5232 (header_up Authorization, reverse_proxy 10.8.0.2:5232); tasks.nixg.ru → Vikunja :3456. Reload Caddyfile.
  • 5.2 Проверить CalDAV/CardDAV снаружи: PROPFIND https://cal.nixg.ru/... → 207 (учесть кириллицу «Контакты» в URL, Auth).
  • 5.3 DAVx⁵ на телефоне: аккаунт https://cal.nixg.ru, логин estorozhenko, пароль RADICALE_PASS → выбрать календари (Личный/Рабочий/Задачи) и адресную книгу «Контакты».
  • 5.4 jtx board на телефоне → Vikunja https://tasks.nixg.ru (токен API), проверить задачи.
  • 5.5 Проверка двусторонней синхронизации: контакт создан/изменён на телефоне → в базе (contacts.json) после contacts_caldav_sync.py; задача из jtx board → в Vikunja; событие из DAVx⁵ → в Radicale.
  • 5.6 (опц.) Вписать sync в contacts-cron.sh (4.1) и проверить V5.

Verification

  • V1: curl -X PROPFIND -u estorozhenko:PASS http://127.0.0.1:5232/estorozhenko/Контакты/ → 207 + addressbook
  • V2: после синка количество vCard в адресной книге == числу контактов с email в contacts.json
  • V3: повторный синк → 0 новых карточек
  • V4: правка контакта на сервере (curl PUT vCard) → контакт в contacts.json обновлён после синка
  • V5: python3 scripts/contacts_extractor.py --sync-caldav из cron-обёртки завершается кодом 0 и пишет caldav-sync.log