From 45617356ee4a2b238e19805f154a15bcbf555e0a Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 14 Sep 2026 15:14:18 -0300 Subject: [PATCH] =?UTF-8?q?docs(plan):=20C164=20+=20C165=20=E2=80=94=20can?= =?UTF-8?q?al=20de=20push=20do=20Google=20Calendar=20e=20importa=C3=A7?= =?UTF-8?q?=C3=A3o=20autom=C3=A1tica=20de=20eventos=20(Related=20#1009,=20?= =?UTF-8?q?#1010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../c164-canal-push-google-expiration.md | 135 ++++ ...c165-importar-eventos-google-ui-draft.html | 584 ++++++++++++++++++ docs/plans/c165-importar-eventos-google.md | 105 ++++ 3 files changed, 824 insertions(+) create mode 100644 docs/plans/c164-canal-push-google-expiration.md create mode 100644 docs/plans/c165-importar-eventos-google-ui-draft.html create mode 100644 docs/plans/c165-importar-eventos-google.md diff --git a/docs/plans/c164-canal-push-google-expiration.md b/docs/plans/c164-canal-push-google-expiration.md new file mode 100644 index 00000000..4005c29c --- /dev/null +++ b/docs/plans/c164-canal-push-google-expiration.md @@ -0,0 +1,135 @@ +# Agenda: canal de push do Google Calendar nunca registra (expiration string → Invalid time value) + +Status: rascunho +Atualizado em: 2026-09-14 +Issue: #1009 +Priority: P1 +Model: composer-2.5 +Impeccable: A — N/A (sem UI) +Rascunho UI: N/A — sem UI +Appetite: ~0,5 dia eng; um outcome verificável — canal de push registra e o vaivém do Google volta a ser imediato +Responsável: — + +## Intenção + +Quem opera a agenda vive no Google Calendar. O Teqo já espelha o que nasce +aqui (atividades criadas, editadas, canceladas aparecem lá), mas o caminho de +volta está morto: edições e cancelamentos feitos no Gmail **não** voltam para a +atividade em tempo real. Na prática, a coordenação/imprensa mexe no evento pelo +Google e o Teqo continua exibindo a versão antiga até alguém abrir a agenda e +disparar um sync manual. + +Descobrimos isso ao vivo: a conexão Google está saudável, o espelho Teqo→Google +funciona, mas o canal de notificação Google→Teqo nunca foi registrado em +produção — `push_channel_id`/`push_channel_expires_at` vazios, `push_channel_error` +preenchido, e o log acusa `Invalid time value` alternando com `fetch failed`. +A causa é determinística: o Google devolve `expiration` como string (int64 em +JSON) e a leitura assume number, quebrando na conversão para data. É um defeito +pequeno com efeito grande: a direção Google→Teqo (C115) fica degradada, e o +usuário percebe como "a agenda não atualiza". + +## Persona e fluxo + +- **Persona / contexto:** coordenação e imprensa do mandato, na mesa, operando a + agenda compartilhada direto no Google Calendar; esperam que o Teqo reflita o + que fazem lá sem passo extra. +- **Job principal:** editar ou cancelar um evento no Google e ver a atividade + correspondente no Teqo atualizar sozinha, em segundos. +- **Fluxo desejado:** (1) o Teqo registra o canal de push na primeira passada de + sync; (2) o canal se renova sozinho perto de expirar; (3) uma edição feita no + Gmail chega como notificação e a atividade aparece atualizada em segundos-minutos, + sem clique em "Sincronizar agora". +- **Anti-goals de produto:** não criar uma segunda fonte de verdade da agenda, + não pausar nem degradar o espelho Teqo→Google quando o canal falhar, não + introduzir UI nova. + +## Objetivo e aceite + +- Após uma passada de sync, o canal de push está registrado: `pushChannelId` e + `pushChannelResourceId` preenchidos, `pushChannelExpiresAt` preenchido e + `pushChannelError` nulo. +- O canal se renova sozinho antes de expirar (lead de renovação já existente), + sem intervenção humana. +- Editar ou cancelar no Gmail um evento espelhado reflete na atividade do Teqo + em segundos-minutos, sem sync manual. +- Guardrail: falha ao registrar/renovar o canal **nunca** derruba o espelho + Teqo→Google nem o restante do Teqo (comportamento best-effort atual mantido); + o erro fica registrado para diagnóstico. +- Sem migração de schema, sem Consent, sem UI nova. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — este item conserta a infraestrutura do espelho + (registro/renovação do canal), sem superfície de dados nova nem métrica de + produto a exibir. O "dado" observável é o estado do canal no banco, usado como + verificação operacional, não como apresentação. +- **Decisões desbloqueadas:** N/A — nenhuma escolha de negócio depende deste + item; o ganho é comportamento (sincronização imediata). +- **Forma:** N/A. + +## Dados da decisão (literais) + +- Contrato do Google: `events.watch` devolve `expiration` como **int64 + serializado em string** no JSON (ex.: `"expiration": "1757900000000"`); o + client hoje tipa `GoogleWatchChannel.expiration: number | null`. +- A normalização deve acontecer **na borda da API** (client), para o resto do + motor continuar recebendo number (ou null) — não espalhar coerção pelo consumidor. +- Fallback de TTL quando `expiration` vier ausente: `PUSH_CHANNEL_TTL_SECONDS`. +- Teste de regressão deve simular a resposta real com `"expiration": ""` + (string entre aspas) e cobrir também `expiration` ausente/nulo → fallback. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/utilities/googleCalendarClient.ts` (tipo e parsing do + watch) e `src/utilities/googleCalendarSync.ts` (único consumidor, conversão + para ISO e catch que grava `pushChannelError`); teste de regressão em + `tests/unit/googleCalendarClient.unit.spec.ts`, cujo stub hoje não modela + `/watch` — a resposta real precisa ser representada. +- **Precedente a olhar:** C115 (canal de push) e C114 (espelho) para o contrato + best-effort; `tests/int/googleCalendarSync.int.spec.ts` e os stubs int/e2e que + hoje só devolvem number/null. +- **Risco de acoplamento:** não alterar o comportamento do espelho Teqo→Google + nem o fail-open do canal; líder/assessor e demais perfis não são afetados. + +## Dependências + +- Nenhuma. + +## Fora de escopo + +- Investigar/retentar o `fetch failed` intermitente no mesmo passo + (transporte) — registrar como observação; só vira trabalho se persistir + depois do fix. +- Mudar o modelo de canal (ex.: renovação por cron dedicado, canal por usuário). +- OAuth / reconexão da conta Google. +- Qualquer UI de status do canal. + +## Rabbit holes de produto + +- **"Já que mexo no canal, melhoro o sync inteiro".** Se alguém "só completar": + reescrever o loop de sync, adicionar full-sync periódico, mexer em OAuth. + **Corte neste item:** corrigir o parsing do `expiration` e provar com teste; + nada além. +- **"O `fetch failed` também é nosso".** Sem causa determinística, perseguir o + erro de transporte agora vira debug aberto. **Corte:** registrá-lo melhor no + `pushChannelError` e reavaliar se persistir. +- **"Mostrar o canal no admin".** Tentação de UI de diagnóstico. **Corte:** o + aceite é verificação operacional (banco + edição real no Gmail). + +## Questões em aberto (produto) + +- **O `pushChannelError` deve carregar mais contexto quando a falha é de + transporte?** **Opções:** A) manter só a mensagem; B) registrar a causa do + fetch (`fetch failed` + detalhe/causa raiz) quando existir. + **Recomendação:** B — ajuda o diagnóstico do `fetch failed` sem custo de + produto, e não muda o guardrail. _(assumido — validar com produto)_ + +## Referências + +- GitHub Issue [#1009](https://github.com/fsolla/teqo/issues/1009) +- Planos C115 (canal de push Google→Teqo) e C114 (espelho de agenda) +- `src/utilities/googleCalendarClient.ts` — tipo/retorno do `watchEvents` +- `src/utilities/googleCalendarSync.ts` — registro/renovação do canal e gravação do erro +- `tests/unit/googleCalendarClient.unit.spec.ts`, `tests/int/googleCalendarSync.int.spec.ts` +- `docs/ops/teqo-1313-deploy.md` — verificação em produção pós-deploy +- `AGENTS.md` — convenção de migrations (aqui: nenhuma) diff --git a/docs/plans/c165-importar-eventos-google-ui-draft.html b/docs/plans/c165-importar-eventos-google-ui-draft.html new file mode 100644 index 00000000..9fcf5b92 --- /dev/null +++ b/docs/plans/c165-importar-eventos-google-ui-draft.html @@ -0,0 +1,584 @@ + + + + + + + UI draft — C165 · Eventos do Google viram atividades + + +
+

+ UI draft — Agenda: importar eventos do Google como atividades (C165) +

+

+ A assessoria cria o compromisso no Google Calendar; ele aparece sozinho em + /campanha/atividades como atividade confirmada, sem município; a coordenação abre o overlay + e atribui o município quando quiser. +

+

+ Cinza = estrutura existente · acento = novo neste item · + barras cinza = conteúdo não especificado. Cenas estáticas: sem interação, sem dados reais. +

+ + + + +
+

+ Cena 1 · desktop (~1280px) — lista /campanha/atividades com importados do Google +

+
+
+
+ + +
+
+
+

Atividades

+

Agenda da campanha · Próximos

+
+ +
+ +
+ Próximos + Todos + Realizados + Rascunhos +
+ +
+ + Sem município × + + Filtrar por janela, tipo, título, município… + 3 resultados +
+ +
+ +
+
+ Confirmado + Do Google + Sem município +
+

+ Nome do compromisso (título como está no Google) +

+
+ + + + + 12/09/2026 · 19:00 + + + + + + + Local informado no Google + +
+
+ + + + + Resp: Não definido + + Importado automaticamente +
+
+ + +
+
+ Confirmado + Do Google + Município · [nome] +
+

+ Outro compromisso (título original do Google) +

+
+ + + + + 15/09/2026 · 09:30 + + + + + + + Local informado no Google + +
+
+ Visível para o assessor da carteira + Importado automaticamente +
+
+ + +
+
+ Confirmado +
+

+ Atividade criada no Teqo +

+
+ + + + + 18/09/2026 · 16:00 + + + + + + + Município · [nome] + +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + + + +
+

+ Cena 2 · desktop — omnibox aberto com a opção "Sem município" (triagem) +

+
+
+ + Sem município × + + sem| +
+
+
+ + novo + Sem município + + 2 atividades +
+
+ Município: [nome do município] + 12 +
+
+ Janela: Próximos 30 dias + — +
+
+ Título contém "caminhada" + 1 +
+
+
+
+ + + + +
+

+ Cena 3 · desktop — overlay de edição com Município opcional e aviso de visibilidade +

+
+
+
+
+
+

Editar atividade

+ Do Google + Sem município +
+

+ Importado do Google Calendar · salvar aqui atualiza o mesmo evento +

+
+ × +
+ +
+
+ Título +
+ Nome do compromisso +
+
+ +
+
+ Início +
+ 12/09/2026 19:00 +
+
+
+ Término (opcional) +
+ 12/09/2026 21:00 +
+
+
+ +
+ + Todo o dia +
+ +
+ + Município (opcional) + novo + +
+ Sem município + ⌄ +
+

+ Sem município, só coordenação e candidato veem. Ao atribuir, o assessor da + carteira passa a ver. +

+
+ +
+ Local (opcional) +
+ Local informado no Google +
+
+ +
+ Responsáveis +
+
+ +
+ Organizações apoiadoras +
+
+
+ +
+ + +
+
+
+
+ + + + +
+

+ Cena 4 · mobile (~390px) — card importado e sheet de edição +

+
+ +
+
+

Atividades

+ Próximos +
+
+ Confirmado + Do Google + Sem município +
+

+ Nome do compromisso (título como está no Google) +

+
+ + + + + 12/09/2026 · 19:00 + + + + + + + Local informado no Google + +
+
+ Resp: Não definido + Importado automaticamente +
+
+ + +
+
+ Editar atividade + × +
+
+
+ Do Google + Sem município +
+
+ Nome do compromisso +
+
+ 12/09/2026 19:00 +
+
+ Município (opcional) +
+ Sem município + ⌄ +
+

+ Sem município, só coordenação e candidato veem. +

+
+
+ Local informado no Google +
+
+ Responsáveis +
+
+
+
+ +
+
+
+
+
+ + diff --git a/docs/plans/c165-importar-eventos-google.md b/docs/plans/c165-importar-eventos-google.md new file mode 100644 index 00000000..6c64756a --- /dev/null +++ b/docs/plans/c165-importar-eventos-google.md @@ -0,0 +1,105 @@ +# Agenda: importar automaticamente eventos do Google como atividades (sem município, a partir de 16/08/2026) + +Status: rascunho +Atualizado em: 2026-09-14 +Issue: #1010 +Priority: P2 +Model: cursor-grok-4.5-high +Depends: C164 +Impeccable: B — encaixe na lista de atividades, no overlay existente e na agenda +Rascunho UI: docs/plans/c165-importar-eventos-google-ui-draft.html +Appetite: ~3–4 dias eng; um outcome verificável — evento criado no Google após o início da campanha vira atividade no Teqo sozinho, e a coordenação atribui o município quando quiser +Responsável: — + +## Intenção + +A agenda da imprensa vive no Google Calendar, mas o Teqo só enxerga dela o que já nasceu no Teqo: o espelho (C114/C115) empurra atividades para o Google e aceita edições de eventos que o próprio Teqo criou — eventos estrangeiros são ignorados de propósito hoje. Daí o pedido do dono: "o que crio no Google não está aparecendo no Teqo". Este item reabre o corte do C115 (criação pelo Google fora de v1; "sem município não entra") para os eventos da conta conectada com início a partir do começo legal da campanha: eles entram como atividade sozinhos, nascem sem município — coisa que o precedente `supporter` já provou ser possível — e a coordenação tria quando quiser. + +## Persona e fluxo + +- **Persona / contexto:** assessoria de imprensa criando compromissos no Google Calendar, muitas vezes pelo celular; coordenação/candidato na mesa fazendo a triagem do que chegou. +- **Job principal:** "o que eu crio no Google aparece no Teqo sozinho; eu só atribuo o município depois". +- **Fluxo desejado:** a assessoria cria o evento no calendário da imprensa → em minutos ele aparece em `/campanha/atividades` como atividade confirmada, com badge "Do Google" e "Sem município" → a coordenação abre o overlay e escolhe o município (opcional, pode ficar sem) → a partir daí a atividade segue as regras normais (advisor da carteira vê) → editar/remarcar/cancelar no Google reflete; excluir no Google cancela a atividade. +- **Anti-goals de produto:** não virar sincronizador universal (convidados, descrição, RSVP, Meet); não criar segundo cadastro nem segunda fila de triagem; não abrir acesso novo à liderança; não apagar/alterar evento estrangeiro; não virar um painel de administração do sync. + +### Esboço de fluxo (B) + +```text +[assessoria cria no Google Calendar] — início ≥ 16/08/2026 +→ motor Google (C164) detecta e importa → atividade `confirmado`, sem município +→ lista/agenda mostram o card com "Do Google" + "Sem município" +→ coordenação triagem: overlay → escolhe o município quando quiser +→ advisor da carteira passa a ver; edições/cancelamento no Google seguem refletindo +``` + +### Rascunho UI (B) + +- Rascunho UI (gate): `docs/plans/c165-importar-eventos-google-ui-draft.html` + +## Objetivo e aceite + +- Evento criado no Google (conta da imprensa já conectada) com início a partir de 16/08/2026 vira atividade no Teqo sozinho, com a imediatez Google→Teqo do C164 — sem ninguém copiar dado. +- O overlay passa a permitir criar/editar atividade sem município; sem município só coordenação/candidato veem; ao atribuir, a atividade entra nas regras normais (advisor por carteira/responsável). +- Vínculo estável e idempotente: re-sync não duplica; editar no Teqo atualiza o MESMO evento do Google — nunca nasce um segundo evento `teqo…`. +- Editar/cancelar no Google reflete na atividade (mesma regra de relógio do C115); excluir no Google cancela a atividade. +- Nada anterior a 16/08/2026 entra (sem backfill); evento estrangeiro nunca é apagado/alterado pelo Teqo; leader lockdown intocado; sem Consent novo; o Teqo segue inteiro se o Google cair. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — superfície operacional (lista, agenda, overlay); sem leitura agregada nova. +- **Decisões desbloqueadas:** coordenação decide o município de um compromisso importado (triagem), o que muda quem o vê; nenhuma métrica. +- **Forma:** adiada ao plano de implementação. + +## Dados da decisão (literais) + +- Corte: início do evento (instante na América/Bahia) ≥ `2026-08-16`; reusar `CALENDAR_PHASE_ANCHORS.consolidationStart` (`src/lib/visitPlannerAnchors.ts:22-29`) — não duplicar o literal nem criar outro corte. Anterior fica fora para sempre. +- Horizonte de varredura: a mesma janela que o espelho já usa (90 dias para trás / 365 para a frente), com o piso do corte; evento fora da janela não é importado nem espelhado na v1. +- Importado nasce `status: confirmado`, título original do Google **sem** o prefixo `[Município] ` (com espaço final; o prefixo é só de evento `teqo…` espelhado). +- Atividade sem município é permitida; visível/editável só por `coordinator`/`candidate` (precedente `supporter`); com município, regras normais. +- Vínculo atividade↔evento Google é system-write (invisível aos user paths): garante idempotência e que editar no Teqo atualiza O MESMO evento. +- Google edita/cancela → atividade (relógio do C115); Google exclui → atividade cancelada. +- `location` do evento → `locality`; descrição ignorada na v1 (texto vive no Google). +- Recorrência: `singleEvents=true` expande; cada ocorrência vira atividade própria. +- Sem `Consent` novo (dado interno de staff); leader lockdown intocado; Teqo fonte da verdade; evento estrangeiro nunca apagado/alterado pelo Teqo. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/collections/Activity.ts` + `src/lib/schemas/activity.ts` (município opcional), `src/utilities/activityFormData.ts`, `src/utilities/access/activities.ts`, motor `src/utilities/googleCalendarSync.ts` (+ `googleCalendarEventMapping.ts`/`googleCalendarReverseEdit.ts`), `src/components/campaign/activity/ActivityOverlay.tsx`, ação em `src/app/(campaign)/campanha/actions/activity.ts`, omnibox `src/utilities/activityOmnibox.ts` + `ActivityFilters.tsx`. +- **Precedente a olhar:** `supporter` com município opcional (`src/utilities/access/supporters.ts:18-41,43-66`; mensagem própria em `src/lib/schemas/supporter.ts:18-19`) e o C115 (relógio/conflito). +- **Risco de acoplamento:** o vínculo com evento estrangeiro precisa nascer (hoje só o id `teqo…` codifica vínculo; não há `googleEventId` em Activity) — schema/migration é decisão do plano de implementação. Pontos que hoje assumem município: ação de rascunhos de demanda (`actions/activity.ts:293-294` dereferencia `.municipality.id`), `CampaignDemand.ts:242-279` ("Município da demanda inválido"), `activityRelationOptions.ts:21-28`; overlay exige município em `ActivityOverlay.tsx:195-205,375-389,459`. Dossiê/filtros por município não mostrarem sem-município é aceitável (a triagem é na lista). +- **Testes que pinam município obrigatório (mudam com o item):** `tests/int/campaignActivity.int.spec.ts:48-55,99-106`, `tests/unit/activityOverlay.unit.spec.tsx:245-258`, `tests/e2e/campaignActivity.e2e.spec.ts:88`. + +## Dependências + +- **Dura: C164** — mesmo motor Google e a imediatez Google→Teqo; sem ela o "sozinho" fica capenga. +- Suaves: C114 (espelho), C115 (bidirecional/relógio; este item reabre o corte de criação), C149/C150 (conta e calendário principal conectados). + +## Fora de escopo + +- Eventos anteriores a 16/08/2026: sem backfill retroativo. +- Descrição, convidados/RSVP, lembretes, Meet e demais metadados do Google não viram campo do Teqo na v1. +- Tela dedicada de triagem/fila de aprovação: a triagem é inline (lista + overlay). +- Importar de calendários secundários/inscritos além do principal conectado. +- Qualquer mudança no leader lockdown ou no feed iCal público. + +## Rabbit holes de produto + +- **Bidirecional total ("já que está lá, sincroniza convidados/descrição/cor").** Se alguém "só completar": a agenda vira espelho integral da API do Google. **Corte:** só vínculo + título/horário/local/cancelamento (relógio C115); descrição fica no Google. +- **Backfill do calendário histórico.** Se alguém "só completar": anos de eventos pessoais/institucionais viram atividade. **Corte:** só início ≥ 16/08/2026, daqui pra frente. +- **Fila de aprovação com tela nova.** Se alguém "só completar": segundo fluxo de triagem, notificações e estados. **Corte:** lista + filtro "Sem município" + overlay existente. +- **Apagar/alterar o evento estrangeiro para "limpar" o Teqo.** Se alguém "só completar": Teqo escrevendo em evento que não criou. **Corte:** exclusão/edição no Teqo nunca toca evento estrangeiro — no máximo cancela a atividade. + +## Questões em aberto (produto) + +- **Quem vê e edita atividade sem município?** **Opções:** A) só coordenação/candidato (precedente `supporter`) | B) + assessores (sem carteira para casar; precisaria de claim) | C) todo staff lê, só coordenação edita. **Recomendação: A.** _(assumido — validar)_ +- **Descrição do evento: importar ou ignorar na v1?** **Opções:** A) ignorar (texto vive no Google) | B) guardar em campo livre/atualização | C) concatenar no `locality`. **Recomendação: A.** _(assumido — validar)_ +- **Cancelar no Teqo remove o evento no Google?** **Opções:** A) sim, cancela o MESMO evento (consistente com "Teqo fonte da verdade") | B) só marca a atividade cancelada e deixa o evento | C) perguntar a cada cancelamento. **Recomendação: A.** +- **Eventos pessoais futuros do calendário da imprensa (aniversário, médico) viram atividade?** **Opções:** A) aceitar e triar (cancelar/descartar na lista) | B) exigir calendário dedicado da campanha | C) allowlist por palavra-chave. **Recomendação: A** — o corte de 16/08 limita o volume e um importado a mais custa pouco; B depende de mudança de hábito externa. +- **Evento importado que ganha município passa a levar `[Município] ` no título do Google?** **Opções:** A) não — mantém o título original (o vínculo system-write basta) | B) sim, vira o padrão dos `teqo…` (confunde quem criou no Google). **Recomendação: A.** + +## Referências + +- GitHub Issue [#1010](https://github.com/fsolla/teqo/issues/1010) +- Rascunho UI (gate): `docs/plans/c165-importar-eventos-google-ui-draft.html` +- `docs/plans/c115-google-calendar-edicao-bidirecional.md:21,65` (corte reaberto), C114, C149, C150 +- `src/lib/visitPlannerAnchors.ts:22-29` (corte canônico), `src/utilities/googleCalendarSync.ts`, `src/utilities/access/supporters.ts`, `AGENTS-campaign.md` ("Campaign activities", "Campaign supporters", "Campaign Municípios model")