mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
docs(plan): C164 + C165 — canal de push do Google Calendar e importação automática de eventos (Related #1009, #1010)
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
This commit is contained in:
parent
ba7aa50346
commit
45617356ee
3 changed files with 824 additions and 0 deletions
135
docs/plans/c164-canal-push-google-expiration.md
Normal file
135
docs/plans/c164-canal-push-google-expiration.md
Normal file
|
|
@ -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": "<millis>"`
|
||||
(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)
|
||||
584
docs/plans/c165-importar-eventos-google-ui-draft.html
Normal file
584
docs/plans/c165-importar-eventos-google-ui-draft.html
Normal file
|
|
@ -0,0 +1,584 @@
|
|||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<title>UI draft — C165 · Eventos do Google viram atividades</title>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 antialiased">
|
||||
<main class="mx-auto max-w-[1400px] p-6 sm:p-8">
|
||||
<h1 class="text-lg font-medium text-zinc-100">
|
||||
UI draft — Agenda: importar eventos do Google como atividades (C165)
|
||||
</h1>
|
||||
<p class="mt-1 max-w-3xl text-sm text-zinc-400">
|
||||
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.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-zinc-500">
|
||||
Cinza = estrutura existente · <span class="text-amber-400">acento</span> = novo neste item ·
|
||||
barras cinza = conteúdo não especificado. Cenas estáticas: sem interação, sem dados reais.
|
||||
</p>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Cena 1 · desktop (~1280px) — lista de atividades -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="mt-8">
|
||||
<p class="text-xs tracking-wide text-zinc-500 uppercase">
|
||||
Cena 1 · desktop (~1280px) — lista /campanha/atividades com importados do Google
|
||||
</p>
|
||||
<div class="mt-3 overflow-x-auto">
|
||||
<div
|
||||
class="w-[1280px] max-w-full overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900"
|
||||
>
|
||||
<div class="flex min-h-[560px]">
|
||||
<aside class="w-52 shrink-0 border-r border-zinc-800 bg-zinc-950/50 p-4">
|
||||
<p class="px-3 text-xs tracking-wide text-zinc-500 uppercase">Campanha</p>
|
||||
<nav class="mt-3 flex flex-col gap-1 text-sm">
|
||||
<span class="rounded-lg px-3 py-2 text-zinc-400">Painel</span>
|
||||
<span class="rounded-lg bg-zinc-800 px-3 py-2 font-medium text-zinc-100"
|
||||
>Atividades</span
|
||||
>
|
||||
<span class="rounded-lg px-3 py-2 text-zinc-400">Municípios</span>
|
||||
<span class="rounded-lg px-3 py-2 text-zinc-400">Lideranças</span>
|
||||
<span class="rounded-lg px-3 py-2 text-zinc-400">Apoiadores</span>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="min-w-0 flex-1 p-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-base font-medium">Atividades</h2>
|
||||
<p class="mt-0.5 text-sm text-zinc-400">Agenda da campanha · Próximos</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="h-11 rounded-lg bg-amber-500 px-4 text-sm font-medium text-zinc-950"
|
||||
>
|
||||
Nova atividade
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2 text-sm">
|
||||
<span class="rounded-lg bg-zinc-800 px-3 py-2 font-medium">Próximos</span>
|
||||
<span class="rounded-lg px-3 py-2 text-zinc-400">Todos</span>
|
||||
<span class="rounded-lg px-3 py-2 text-zinc-400">Realizados</span>
|
||||
<span class="rounded-lg px-3 py-2 text-zinc-400">Rascunhos</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 flex flex-wrap items-center gap-2 rounded-lg border border-zinc-800 bg-zinc-950 p-2"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md bg-zinc-800 px-2 py-1 text-xs text-zinc-200"
|
||||
>
|
||||
Sem município <span class="text-zinc-500">×</span>
|
||||
</span>
|
||||
<span class="px-1 text-sm text-zinc-500"
|
||||
>Filtrar por janela, tipo, título, município…</span
|
||||
>
|
||||
<span class="ml-auto px-1 text-xs text-zinc-500">3 resultados</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
<!-- Card importado, ainda sem município -->
|
||||
<article class="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="rounded-md border border-zinc-700 px-2 py-0.5 text-xs text-zinc-300"
|
||||
>Confirmado</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-400"
|
||||
>Do Google</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-dashed border-zinc-600 px-2 py-0.5 text-xs text-zinc-400"
|
||||
>Sem município</span
|
||||
>
|
||||
</div>
|
||||
<h3
|
||||
class="mt-3 text-sm font-medium text-zinc-100 underline decoration-zinc-700 underline-offset-4"
|
||||
>
|
||||
Nome do compromisso (título como está no Google)
|
||||
</h3>
|
||||
<div class="mt-2 flex flex-col gap-1.5 text-sm text-zinc-400">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
12/09/2026 · 19:00
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path d="M20 10c0 6-8 12-8 12S4 16 4 10a8 8 0 1 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
Local informado no Google
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 flex flex-wrap items-center justify-between gap-2 border-t border-zinc-800 pt-3 text-sm text-zinc-400"
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path d="M20 21a8 8 0 0 0-16 0M12 3a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z" />
|
||||
</svg>
|
||||
Resp: Não definido
|
||||
</span>
|
||||
<span class="text-xs text-zinc-500">Importado automaticamente</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Card importado já triado (município atribuído) -->
|
||||
<article class="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="rounded-md border border-zinc-700 px-2 py-0.5 text-xs text-zinc-300"
|
||||
>Confirmado</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-400"
|
||||
>Do Google</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-zinc-700 px-2 py-0.5 text-xs text-zinc-300"
|
||||
>Município · [nome]</span
|
||||
>
|
||||
</div>
|
||||
<h3
|
||||
class="mt-3 text-sm font-medium text-zinc-100 underline decoration-zinc-700 underline-offset-4"
|
||||
>
|
||||
Outro compromisso (título original do Google)
|
||||
</h3>
|
||||
<div class="mt-2 flex flex-col gap-1.5 text-sm text-zinc-400">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
15/09/2026 · 09:30
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path d="M20 10c0 6-8 12-8 12S4 16 4 10a8 8 0 1 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
Local informado no Google
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 flex flex-wrap items-center justify-between gap-2 border-t border-zinc-800 pt-3 text-sm text-zinc-400"
|
||||
>
|
||||
<span class="text-xs text-zinc-500">Visível para o assessor da carteira</span>
|
||||
<span class="text-xs text-zinc-500">Importado automaticamente</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Card comum do Teqo (sem Google) -->
|
||||
<article class="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="rounded-md border border-zinc-700 px-2 py-0.5 text-xs text-zinc-300"
|
||||
>Confirmado</span
|
||||
>
|
||||
</div>
|
||||
<h3
|
||||
class="mt-3 text-sm font-medium text-zinc-100 underline decoration-zinc-700 underline-offset-4"
|
||||
>
|
||||
Atividade criada no Teqo
|
||||
</h3>
|
||||
<div class="mt-2 flex flex-col gap-1.5 text-sm text-zinc-400">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
18/09/2026 · 16:00
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path d="M20 10c0 6-8 12-8 12S4 16 4 10a8 8 0 1 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
Município · [nome]
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Placeholder de conteúdo não especificado -->
|
||||
<article
|
||||
class="flex flex-col justify-center gap-3 rounded-lg border border-dashed border-zinc-800 bg-zinc-900/50 p-4"
|
||||
>
|
||||
<div class="h-4 w-32 rounded bg-zinc-800"></div>
|
||||
<div class="h-3 w-56 rounded bg-zinc-800/80"></div>
|
||||
<div class="h-3 w-40 rounded bg-zinc-800/60"></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Cena 2 · desktop — omnibox aberto com "Sem município" -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="mt-10">
|
||||
<p class="text-xs tracking-wide text-zinc-500 uppercase">
|
||||
Cena 2 · desktop — omnibox aberto com a opção "Sem município" (triagem)
|
||||
</p>
|
||||
<div class="mt-3 w-[760px] max-w-full rounded-xl border border-zinc-800 bg-zinc-900 p-6">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 rounded-lg border border-zinc-800 bg-zinc-950 p-2"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md bg-zinc-800 px-2 py-1 text-xs text-zinc-200"
|
||||
>
|
||||
Sem município <span class="text-zinc-500">×</span>
|
||||
</span>
|
||||
<span class="px-1 text-sm text-zinc-300">sem<span class="text-zinc-600">|</span></span>
|
||||
</div>
|
||||
<div class="mt-2 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-950">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-l-2 border-amber-500 bg-amber-500/10 px-3 py-2 text-sm"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded-md border border-amber-500/40 px-1.5 py-0.5 text-[10px] font-medium tracking-wide text-amber-400 uppercase"
|
||||
>novo</span
|
||||
>
|
||||
<span class="text-zinc-100">Sem município</span>
|
||||
</span>
|
||||
<span class="text-xs text-zinc-500">2 atividades</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-t border-zinc-800 px-3 py-2 text-sm text-zinc-400"
|
||||
>
|
||||
<span>Município: [nome do município]</span>
|
||||
<span class="text-xs text-zinc-600">12</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-t border-zinc-800 px-3 py-2 text-sm text-zinc-400"
|
||||
>
|
||||
<span>Janela: Próximos 30 dias</span>
|
||||
<span class="text-xs text-zinc-600">—</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-t border-zinc-800 px-3 py-2 text-sm text-zinc-400"
|
||||
>
|
||||
<span>Título contém "caminhada"</span>
|
||||
<span class="text-xs text-zinc-600">1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Cena 3 · desktop — overlay de edição (Município opcional) -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="mt-10">
|
||||
<p class="text-xs tracking-wide text-zinc-500 uppercase">
|
||||
Cena 3 · desktop — overlay de edição com Município opcional e aviso de visibilidade
|
||||
</p>
|
||||
<div
|
||||
class="mt-3 overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950/70 p-6 sm:p-10"
|
||||
>
|
||||
<div class="mx-auto max-w-2xl rounded-xl border border-zinc-800 bg-zinc-900">
|
||||
<div class="flex items-start justify-between gap-3 border-b border-zinc-800 p-4">
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h2 class="text-sm font-medium">Editar atividade</h2>
|
||||
<span
|
||||
class="rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-400"
|
||||
>Do Google</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-dashed border-zinc-600 px-2 py-0.5 text-xs text-zinc-400"
|
||||
>Sem município</span
|
||||
>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-zinc-500">
|
||||
Importado do Google Calendar · salvar aqui atualiza o mesmo evento
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-zinc-500">×</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Título</span>
|
||||
<div
|
||||
class="flex h-11 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-200"
|
||||
>
|
||||
Nome do compromisso
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Início</span>
|
||||
<div
|
||||
class="flex h-11 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-200"
|
||||
>
|
||||
12/09/2026 19:00
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Término (opcional)</span>
|
||||
<div
|
||||
class="flex h-11 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-200"
|
||||
>
|
||||
12/09/2026 21:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex h-11 items-center gap-2 rounded-lg border border-zinc-800 px-3">
|
||||
<span class="size-4 rounded border border-zinc-600"></span>
|
||||
<span class="text-sm text-zinc-300">Todo o dia</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="flex items-center gap-2 text-sm text-zinc-300">
|
||||
Município (opcional)
|
||||
<span
|
||||
class="rounded-md border border-amber-500/40 px-1.5 py-0.5 text-[10px] font-medium tracking-wide text-amber-400 uppercase"
|
||||
>novo</span
|
||||
>
|
||||
</span>
|
||||
<div
|
||||
class="flex h-11 items-center justify-between rounded-lg border border-amber-500/50 px-3 text-sm"
|
||||
>
|
||||
<span class="text-zinc-500">Sem município</span>
|
||||
<span class="text-zinc-600">⌄</span>
|
||||
</div>
|
||||
<p class="text-xs text-amber-400/90">
|
||||
Sem município, só coordenação e candidato veem. Ao atribuir, o assessor da
|
||||
carteira passa a ver.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Local (opcional)</span>
|
||||
<div
|
||||
class="flex h-11 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-200"
|
||||
>
|
||||
Local informado no Google
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Responsáveis</span>
|
||||
<div
|
||||
class="h-11 rounded-lg border border-dashed border-zinc-800 bg-zinc-950/60"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Organizações apoiadoras</span>
|
||||
<div
|
||||
class="h-11 rounded-lg border border-dashed border-zinc-800 bg-zinc-950/60"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 border-t border-zinc-800 p-4">
|
||||
<button
|
||||
type="button"
|
||||
class="h-11 rounded-lg border border-zinc-700 px-4 text-sm text-zinc-300"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-11 rounded-lg bg-amber-500 px-4 text-sm font-medium text-zinc-950"
|
||||
>
|
||||
Salvar atividade
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Cena 4 · mobile (~390px) — card e sheet -->
|
||||
<!-- ============================================================ -->
|
||||
<section class="mt-10">
|
||||
<p class="text-xs tracking-wide text-zinc-500 uppercase">
|
||||
Cena 4 · mobile (~390px) — card importado e sheet de edição
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap items-start gap-6">
|
||||
<!-- Card mobile -->
|
||||
<div class="w-[390px] max-w-full rounded-xl border border-zinc-800 bg-zinc-900 p-4">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="text-sm font-medium">Atividades</h2>
|
||||
<span class="text-xs text-zinc-500">Próximos</span>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-md border border-zinc-700 px-2 py-0.5 text-xs text-zinc-300"
|
||||
>Confirmado</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-400"
|
||||
>Do Google</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-dashed border-zinc-600 px-2 py-0.5 text-xs text-zinc-400"
|
||||
>Sem município</span
|
||||
>
|
||||
</div>
|
||||
<h3 class="mt-3 text-sm font-medium text-zinc-100">
|
||||
Nome do compromisso (título como está no Google)
|
||||
</h3>
|
||||
<div class="mt-2 flex flex-col gap-1.5 text-sm text-zinc-400">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
|
||||
/>
|
||||
</svg>
|
||||
12/09/2026 · 19:00
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="size-4 shrink-0"
|
||||
>
|
||||
<path d="M20 10c0 6-8 12-8 12S4 16 4 10a8 8 0 1 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
Local informado no Google
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 flex items-center justify-between border-t border-zinc-800 pt-3 text-xs text-zinc-500"
|
||||
>
|
||||
<span>Resp: Não definido</span>
|
||||
<span>Importado automaticamente</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sheet mobile -->
|
||||
<div
|
||||
class="w-[390px] max-w-full overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-zinc-800 px-4 py-3">
|
||||
<span class="text-sm font-medium">Editar atividade</span>
|
||||
<span class="text-zinc-500">×</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-400"
|
||||
>Do Google</span
|
||||
>
|
||||
<span
|
||||
class="rounded-md border border-dashed border-zinc-600 px-2 py-0.5 text-xs text-zinc-400"
|
||||
>Sem município</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-11 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-200"
|
||||
>
|
||||
Nome do compromisso
|
||||
</div>
|
||||
<div
|
||||
class="flex h-11 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-200"
|
||||
>
|
||||
12/09/2026 19:00
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Município (opcional)</span>
|
||||
<div
|
||||
class="flex h-11 items-center justify-between rounded-lg border border-amber-500/50 px-3 text-sm"
|
||||
>
|
||||
<span class="text-zinc-500">Sem município</span>
|
||||
<span class="text-zinc-600">⌄</span>
|
||||
</div>
|
||||
<p class="text-xs text-amber-400/90">
|
||||
Sem município, só coordenação e candidato veem.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-11 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-200"
|
||||
>
|
||||
Local informado no Google
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-300">Responsáveis</span>
|
||||
<div
|
||||
class="h-11 rounded-lg border border-dashed border-zinc-800 bg-zinc-950/60"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-zinc-800 p-4">
|
||||
<button
|
||||
type="button"
|
||||
class="h-11 w-full rounded-lg bg-amber-500 text-sm font-medium text-zinc-950"
|
||||
>
|
||||
Salvar atividade
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
105
docs/plans/c165-importar-eventos-google.md
Normal file
105
docs/plans/c165-importar-eventos-google.md
Normal file
|
|
@ -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")
|
||||
Loading…
Reference in a new issue