Convertere

Wdrożenie: synchronizacja kalendarzy Radicale → Basecamp

Data: 2026-09-03 | Wariant A1 (decyzja Kuby) | Status: wdrożone, testy zakończone

Co powstało

One-way sync Radicale (CalDAV) → Basecamp Schedule + (decyzja Kuby z 2026-09-03) reverse sync BC → Radicale dla wpisów zarządzanych przez sync.

Parametry

ParametrWartość
Projekt BC „Rodzinne"bucket 48748049
Schedule „Calendar"10266228493
KalendarzeN, Rodzina, K-Praca, K, K-Sport (666166646 pominięty — 0 eventów)
Prefiksy tytułów[N], [Rodzina], [K-Praca], [K], [K-Sport]
Okno rolling[-30 d; +180 d]
Do BCtylko tytuł + daty (bez DESCRIPTION/VALARM/uczestników; LOCATION nie)
Throttle≤5 req/s, Retry-After, backoff 5xx (max 5)
Timeout cyklu10 min (20 przy --full); kill bezpieczny
Kolejność cyklutrashes → creates → updates (mutacje = trash+create, patrz niżej)

Wyniki liczbowe (stan po wdrożeniu)

KalendarzWpisów w BCUwagi
N16
Rodzina179w tym 120 wystąpień recurring
K-Praca4w tym 4 recurring (rocznice/urodziny)
K06 eventów w źródle, wszystkie poza oknem (2025-04…2026-05) — zweryfikowane pełnym listingiem
K-Sport074 eventy w źródle (plan 6-tyg. 2025-10…11), wszystkie poza oknem
Razem199zero duplikatów

Wyniki weryfikacji endpointów BC (PoC, realne wywołania)

Sync dwukierunkowy (decyzja Kuby 2026-09-03, nadpisuje one-way ze spec)

Forward (Radicale→BC) działa jak wyżej. Dodatkowo reverse (BC→Radicale):

Gwarancja przetrwania („pewnego dnia zostanie tylko Basecamp")

Testy weryfikacyjne (wszystkie wykonane realnie)

TestWynik
(a) zmiana tytułu wpisu [N] w BC → po cyklu zmiana SUMMARY w Radicale
(b) usunięcie wpisu [N] w BC → po cyklu DELETE z Radicale (HTTP 404 po)
(c) wpis bez prefiksu w BC → zero reakcji syncu (unclaimed=1)
(d) Radicale down (martwy port w configu) → cykl NO-OP, licznik porażek, stan BC bez zmian
(e) przywrócenie Radicale → sync wznawia, zero duplikatów
DST: 19:15 CEST → 17:15 UTC w BC ([Rodzina] Jozef stomatolog)
DST: testowy event 19:15 CET (2026-12-10) → 18:15 UTC w BC
Edycja w Radicale widoczna w BC ≤ 20 min (cron 15 min)✅ strukturalnie + test tytułu
Usunięcie → trash ≤ 20 min
Zero duplikatów po 3+ restartach, w tym kill -9 w trakcie cyklu (Rodzina)✅ (199 wpisów, duplikaty: 0)
RRULE → osobne wpisy per wystąpienie✅ (26+25+25+25+13+… wystąpień)
RECURRENCE-ID nadpisania → osobne wpisy✅ (2 szt.)
Sekrety poza repo/logami (grep /opt, /root/convertere, logi sync)
Put/Delete do Radicale (reverse)✅ (201/200)

Naprawione w trakcie wdrożenia

  1. EXDATE parsingvDDDLists nie jest iterowalny pojedynczo; obsługa listy właściwości.
  2. All-day w BC — API odrzuca YYYY-MM-DD; format T00:00:00Z/T23:59:59Z (RFC3339).
  3. Stabilny hash między procesami — wbudowany hash() jest solony per proces (powodowałby rebuild wszystkiego co cykl); zastąpiony sha256.
  4. Reverse nie niszczy rekurencji — PUT z Radicale zachowuje RRULE/EXDATE istniejącego VEVENT (wykryty i naprawiony przypadek utraty RRULE na all-day rocznicy — odtworzone ręcznie, stan spójny).
  5. Reverse delete bug indeksowania — kolumna detached czytana jawnym SELECT-em.

Ograniczenia ( jawne )

Logowanie do czatu AI log (wymaganie Kuby 2026-09-03, dodane po wdrożeniu)

Każda akcja syncu idzie na czat Basecamp „AI log" (room 10271281621, bucket 48748049) przez moduł /opt/radicale2basecamp/bc_log.py. Kanał dodatkowy — sync.log działa bez zmian.

Testy czatu (realne, 2026-09-03)

TestWynik
(a) ręczny cykl → START, CREATE, TRASH, END widoczne na czacie
(b) wymuszony create (testowy event PUT do Radicale) → ✅ CREATE [N] … na czacie; usunięcie → ✅ TRASH [N] …
(c) sync.log nadal pełny (INFO, struktura bez zmian)
(d) idempotencja: 2 cykle bez zmian → tylko START+END („bez zmian (199 unchanged)")
fail-safe: cykl z błędnym roomem → sync OK, 2× WARNING w sync.log
cron_wrapper.sh ręcznie po modyfikacji → END na czacie
py_compile wszystkich modułów

Runbook operacyjny