O events.watch serializa o expiration (int64 epoch ms) como string JSON;
o client tipava number|null e o motor lancava RangeError: Invalid time
value no new Date(...).toISOString(), gravando pushChannelError sem
persistir o canal.
- parseGoogleWatchExpiration (borda do client) normaliza string|number
para number|null com teto do range do Date; malformado cai no fallback
de PUSH_CHANNEL_TTL_SECONDS, nunca lanca.
- pushChannelError ganha a causa do fetch (error.cause) preservada no
teto de 500 chars.
- Unit: stub de transporte modela /watch (string real) + matriz do
parser; int: caminho number->ISO, fallback null->now+TTL e causa.
Closes#1009
Os afterChange/afterDelete de activity e googleCalendarSync rodavam a reconciliacao completa dentro da transacao do save, segurando row lock + pool por todo o I/O do Google (15s por hop, N RTTs -> 45-75s). Corrige com deadline proprio do hook (GOOGLE_CALENDAR_SYNC_HOOK_TIMEOUT_MS=5s) via AbortSignal.timeout combinado com per-hop 15s no cliente (AbortSignal.any) — manual/webhook mantem 15s sem transacao como fallback confiavel. Fail-closed preservado (abort vira paused). Pin unit hook < per-hop + int hook <=7s com fetch lento.
- runSyncPass bidirecional: regra do relógio decide direção (Google mais novo
aplica título/horário; Teqo re-asserta), cancelamento via showDeleted +
snapshot lastSeenEventIds, registro no updates feed
- webhook público /campanha/agenda/google-webhook/[secret] fail-closed
(secret + X-Goog headers constant-time), canal lazy sem cron (ensure em
toda passada, renovação com id novo + stop do antigo)
- migration hand-written (campos de canal/snapshot em googleCalendarSync);
mutationKind dedicado libera título via Google sem quebrar slug canônico
- UI: seção 'Edições pelo Google' no dialog + autor 'Google Calendar' no feed
- testes: unit (reverse mapping, validação webhook), int (motor bidirecional
com stub, 10 cenários), e2e (webhook 404 fail-closed)
Espelho oficial da agenda em calendário Google compartilhado via API
(service account, jose + fetch): reconciliação por passada com id
determinístico teqo+base32hex (upsert idempotente, sem estado por evento),
hooks afterChange/afterDelete na activity (só campos espelhados, nunca
lança), auto-resync quando calendarId muda, retry na view da agenda,
pill de estado no header + FAB + diálogo (4 estados). Cancelado no Teqo
deleta o evento no Google. Sem participantes no v1 (decisão de produto).
Sem migration nova de tabela (hand-written). Closes#635