docs(plans): C147–C151 — sync Google Calendar, modais da agenda, OAuth, calendário principal, evento público

This commit is contained in:
Francisco Solla 2026-08-27 18:14:38 -03:00
parent 1a6bd61462
commit 3dba17b485
9 changed files with 1480 additions and 0 deletions

View file

@ -0,0 +1,82 @@
# Google Calendar: sync configurado em produção não funciona (diagnóstico + fix)
Status: rascunho
Atualizado em: 2026-08-27
Issue: #932
Priority: P1
Model: composer-2.5
Impeccable: A — N/A (sem UI nova; a pill/dialog de estado já existem)
Rascunho UI: N/A — sem UI
Appetite: ~1–2 dias eng; um outcome verificável — com a integração configurada no Google e no Teqo, criar/editar uma atividade reflete no calendário em minutos e o estado do sync é honesto (nunca silenciosamente quebrado)
Responsável: —
## Intenção
"Eu configurei a integração do Google Calendar para a agenda da campanha em produção, mas ela não funciona — não está sincronizando. Descubra por que e conserte." A integração foi entregue (motor de reconciliação bidirecional, pill de estado, webhook), mas na prática a agenda oficial da campanha e o Google Calendar não estão conversando — e nada na interface diz por que. O problema não é "falta feature": é uma integração existente que falha em silêncio. Este item existe para (1) diagnosticar a causa em produção, (2) consertá-la e (3) garantir que, daqui em diante, uma falha de sync nunca passe despercebido — o estado mostrado na agenda precisa ser honesto, inclusive quando o canal de push do Google expira sem ninguém perceber.
## Persona e fluxo
- **Persona / contexto:** o coordenador (ou o próprio deputado) que mantém `/campanha/agenda` como fonte da agenda oficial; já configurou a service account no Google Cloud e o calendário no Teqo, e confia que "configurado" significa "funcionando".
- **Job principal:** criar/editar uma atividade no Teqo e vê-la aparecer no calendário do Google em minutos — ou, se não aparecer, saber imediatamente o motivo na própria agenda.
- **Fluxo desejado:** configura a integração uma vez → usa a agenda normalmente → quando algo quebra (credencial revogada, canal de push expirado, calendarId errado), a pill de estado diz o que aconteceu em vez de fingir que está tudo bem → o coordenador (ou o runbook) resolve sem depender de sorte ou de uma passada de sync que ninguém disparou.
- **Anti-goals de produto:** não virar um projeto de OAuth de usuário (troca de modelo de credencial), não redesenhar os modais de estado, não reconstruir o motor de sync que já existe e funciona em dev.
## Objetivo e aceite
- Com a integração configurada no Google e no Teqo, criar ou editar uma atividade em `/campanha/atividades` reflete no calendário do Google em minutos.
- O estado do sync exibido na agenda (pill + dialog existentes) é honesto: se o sync está quebrado, ele diz que está quebrado e por quê — nunca "sincronizado" quando não está, nunca silêncio.
- Se o canal de push do Google expirar e nenhuma passada de sync rodar para renová-lo, existe um caminho de renovação que não depende de sorte (hipótese a validar no diagnóstico).
- Guardrails: o Teqo permanece a fonte da verdade — uma falha do Google nunca corrompe nem bloqueia os dados de atividade no Teqo; a integração continua opcional e fail-closed (sem credencial → agenda funciona normal, estado "não configurado"); o lockdown de liderança e o modelo de municípios ficam intocados.
## Dados (intenção)
- **Vou apresentar dados?** Não — este item não cria superfície de dados nova; usa o estado já registrado no doc de sync (lastError, lastErrorAt, pushChannelError) como insumo do diagnóstico.
- **Decisões desbloqueadas:** coordenador/operador decide, ao ler o estado honesto, se o problema é configuração no Google, env no servidor ou canal de push — sem precisar abrir o banco.
- **Forma:** adiada ao plano de implementação — aqui só a restrição: o estado precisa ser legível por um humano não-técnico na UI existente.
## Dados da decisão (literais)
- `GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY` — base64 do JSON da service account GCP (conforme `.env.example:61–67: "cat key.json | base64 -w0"`; a service account precisa de permissão "make changes to events" no calendário da campanha). Em produção, o valor vive em `~/stack/teqo-1313.env` no homeserver.
- Comportamento fail-closed já travado: env ausente/malformada → credencial `null` → status derivado `not-configured` → agenda do Teqo segue funcionando sem espelho no Google; nenhum erro além da pill.
- O `calendarId` é configurado por admin no doc único da collection de sync (campo admin-only); um valor errado aqui é causa plausível de "configurado mas não sincroniza".
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/utilities/googleCalendarSync.ts` (motor de reconciliação), `src/utilities/googleCalendarClient.ts` (JWT via `jose`, timeout 15s/hop), `src/utilities/googleCalendarSyncHooks.ts` (hooks afterChange/afterDelete de `activity`, budget de 5s — C114-LOCK), `src/app/(campaign)/campanha/agenda/google-webhook/[secret]/route.ts` (push), `src/components/campaign/agenda/AgendaGoogleSyncChrome.tsx` + `GoogleCalendarSyncDialog.tsx` (estado exibido), `src/collections/GoogleCalendarSync.ts` (doc único, `calendarId`).
- **Hipóteses de causa-raiz a testar, em ordem de barato→caro:** env ausente no container de produção (fail-closed engoliu o problema); service account sem compartilhamento no calendário ou credencial revogada; `calendarId` errado; canal de push expirado (30 dias) sem passada que o renove; webhook inalcançável atrás do túnel Cloudflare (`getCampaignInviteBaseURL`/`NEXT_PUBLIC_SITE_URL`).
- **Precedente a olhar:** docs/plans/c114-google-calendar-push-agenda-oficial.md (+ -impl), c115 (bidirecional), c114-lock (budget de hooks), c122; Issues #635/#636 (closed).
- **Risco de acoplamento:** o budget de 5s dos hooks (C114-LOCK) existe para o write path nunca esperar o Google — o guardrail "falha não é silenciosa" deve vir de telemetria/estado, não de deixar o hook lento; não reconstruir o motor, não trocar o modelo de credencial (C149 é outro item).
## Dependências
- Nenhuma dura. C114/C115 (entregues, closed) são pré-condição de código — já estão em `main`/produção.
## Fora de escopo
- C149 — troca do modelo de credencial para OAuth de usuário (outro item; aqui a service account permanece).
- C148 — layout/redesign dos modais de estado (aqui só se garante que o conteúdo do estado é honesto).
- Reconstrução do motor de reconciliação bidirecional; introdução de cron/queue como infra nova (só se o diagnóstico provar ser a causa-raiz mínima).
## Rabbit holes de produto
- **"Já que vou tocar no sync, implemento OAuth com conta de usuário do Google."** Se alguém "só completar": reescreve o modelo de credencial inteiro, atrasa o fix de um bug P1. **Corte neste item:** service account permanece; troca é C149.
- **"Aproveito e redesenho a pill/dialog do sync."** Se alguém "só completar": projeto de UI para mascarar um problema de backend. **Corte neste item:** estado honesto no conteúdo; forma é C148.
- **"Diagnóstico virou auditoria do motor inteiro + suíte de e2e de sync."** Se alguém "só completar": semana de engenharia para um outcome de 1–2 dias. **Corte neste item:** diagnóstico orientado pelo checklist (env → credencial → calendarId → estado do doc/pushChannelError → canal de push → webhook), fix da causa-raiz e o guardrail de não-silêncio.
## Questões em aberto (produto)
- **O fix pode incluir operação no homeserver (p. ex. env faltando em `~/stack/teqo-1313.env`, reinício de container)?** **Opções:** A) sim, e o que for configuração vira linha no runbook; B) só código, operação fica com humano. **Recomendação:** A — se a causa-raiz for configuração, registrar o passo no runbook `docs/ops/teqo-1313-deploy.md` para não depender de memória. _(assumido — validar com produto)_
- **Quer telemetria mínima no log do servidor quando o hook de sync aborta (hoje é `logger.debug`/estado no doc, invisível no write path)?** **Opções:** A) log de erro estruturado no abort; B) manter só estado no doc. **Recomendação:** A — a falha silenciosa foi exatamente o sintoma relatado; log com nível de erro no abort é a mudança mínima que fecha a classe do problema.
- **A renovação do canal de push precisa de gatilho garantido (a cada passada vira "renova só se algo disparar passada")?** **Opções:** A) renovação atrelada a um gatilho que comprovadamente roda (a auto-retry no load da agenda, se diagnosticada como suficiente); B) infra mínima de cron. **Recomendação:** A — cron/queue só se o diagnóstico provar que nenhum gatilho existente roda com frequência suficiente; decidir depois do diagnóstico, não antes.
## Referências
- GitHub Issue: #— (a registrar via `pnpm agent:register`)
- `src/utilities/googleCalendarSync.ts`, `src/utilities/googleCalendarClient.ts`, `src/utilities/googleCalendarSyncHooks.ts`
- `src/collections/GoogleCalendarSync.ts`, `src/collections/Activity.ts`
- `src/app/(campaign)/campanha/agenda/google-webhook/[secret]/route.ts`
- `src/components/campaign/agenda/AgendaGoogleSyncChrome.tsx`, `GoogleCalendarSyncDialog.tsx`
- `.env.example:61–67` — literal e contrato fail-closed do env
- docs/plans/c114-google-calendar-push-agenda-oficial.md (+ -impl), docs/plans/c115-google-calendar-edicao-bidirecional.md (+ -impl), docs/plans/c114-lock-hooks-do-sync-do-google-calendar-seguram-impl.md
- `docs/ops/teqo-1313-deploy.md` — runbook do homeserver (destino das conclusões de configuração)
- `AGENTS.md` — produção = homeserver (`teqo_1313`), env em `~/stack/teqo-1313.env`, sem Vercel

View file

@ -0,0 +1,376 @@
<!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 — C148 · Modais da agenda no desktop</title>
</head>
<body class="bg-zinc-950 text-zinc-100 antialiased">
<main class="mx-auto max-w-6xl px-6 py-10">
<h1 class="text-lg font-semibold">C148 · Modais da agenda usáveis no desktop</h1>
<p class="mt-1 text-sm text-zinc-400">
Assessor/coordenador no desktop, criando atividade e sincronizando a agenda. Zinc neutro +
um acento (indigo) só no CTA primário. Estático — só medidas e encaixe.
</p>
<!-- Cena 1 · desktop ~1280px · criação de atividade -->
<section class="mt-10">
<p class="text-xs font-medium uppercase tracking-wider text-zinc-500">
Cena 1 · Desktop ~1280px — modal de criação de atividade
</p>
<p class="mt-1 text-xs text-zinc-500">
Largura generosa, paddings contidos, Início/Término lado a lado (cada um com data+hora na
mesma linha), corpo com rolagem interna e rodapé fixo.
</p>
<div class="mt-3 rounded-xl border border-zinc-800 bg-zinc-900/40 p-6 sm:p-10">
<div class="space-y-2 opacity-40" aria-hidden="true">
<div class="h-3 w-48 rounded bg-zinc-800"></div>
<div class="h-3 w-32 rounded bg-zinc-800"></div>
</div>
<div class="mt-6 flex justify-center">
<div
class="w-full max-w-3xl overflow-hidden rounded-xl border border-zinc-700 bg-zinc-900"
>
<div class="flex items-center justify-between border-b border-zinc-800 px-5 py-4">
<h3 class="text-sm font-semibold">Nova atividade</h3>
<span class="text-lg leading-none text-zinc-500">×</span>
</div>
<div class="max-h-80 overflow-y-auto px-5 py-5">
<div class="grid gap-4">
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Título</span>
<div
class="flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm text-zinc-600"
>
Título da atividade
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Tipo</span>
<div
class="flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm"
>
<span class="text-zinc-600">Selecione o tipo</span
><span class="text-zinc-600">▾</span>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Status</span>
<div
class="flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm"
>
<span class="text-zinc-600">Selecione o status</span
><span class="text-zinc-600">▾</span>
</div>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Município</span>
<div
class="flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm"
>
<span class="text-zinc-600">Selecione o município</span
><span class="text-zinc-600">▾</span>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Localidade</span>
<div
class="flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm text-zinc-600"
>
Bairro, praça ou referência
</div>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Início</span>
<div class="flex gap-2">
<div
class="flex h-11 min-w-0 flex-1 items-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm text-zinc-600"
>
dd/mm/aaaa
</div>
<div
class="flex h-11 w-16 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">--</span><span class="text-zinc-600">▾</span>
</div>
<div
class="flex h-11 w-16 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">--</span><span class="text-zinc-600">▾</span>
</div>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Término</span>
<div class="flex gap-2">
<div
class="flex h-11 min-w-0 flex-1 items-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm text-zinc-600"
>
dd/mm/aaaa
</div>
<div
class="flex h-11 w-16 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">--</span><span class="text-zinc-600">▾</span>
</div>
<div
class="flex h-11 w-16 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">--</span><span class="text-zinc-600">▾</span>
</div>
</div>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400"
>Assessores responsáveis</span
>
<div class="flex flex-wrap gap-2">
<div class="h-9 w-28 rounded-lg bg-zinc-800"></div>
<div class="h-9 w-36 rounded-lg bg-zinc-800"></div>
<div
class="flex h-9 items-center rounded-lg border border-dashed border-zinc-700 px-3 text-xs text-zinc-500"
>
Adicionar
</div>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400"
>Organizações apoiadoras</span
>
<div class="flex flex-wrap gap-2">
<div class="h-9 w-32 rounded-lg bg-zinc-800"></div>
<div class="h-9 w-24 rounded-lg bg-zinc-800"></div>
<div class="h-9 w-40 rounded-lg bg-zinc-800"></div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 border-t border-zinc-800 px-5 py-4">
<div
class="flex h-11 items-center rounded-lg border border-zinc-700 px-4 text-sm font-medium text-zinc-200"
>
Cancelar
</div>
<div
class="flex h-11 items-center rounded-lg bg-indigo-600 px-4 text-sm font-medium text-white"
>
Salvar atividade
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Cena 2 · desktop · sync Google -->
<section class="mt-10">
<p class="text-xs font-medium uppercase tracking-wider text-zinc-500">
Cena 2 · Desktop — modal de sincronização Google (feed ICS segue o mesmo padrão)
</p>
<p class="mt-1 text-xs text-zinc-500">
Corpo com altura máxima e rolagem interna: a lista longa de opções não vaza para fora da
tela; os botões ficam fixos no rodapé.
</p>
<div class="mt-3 rounded-xl border border-zinc-800 bg-zinc-900/40 p-6 sm:p-10">
<div class="flex justify-center">
<div
class="w-full max-w-xl overflow-hidden rounded-xl border border-zinc-700 bg-zinc-900"
>
<div class="flex items-center justify-between border-b border-zinc-800 px-5 py-4">
<h3 class="text-sm font-semibold">Sincronizar com Google Agenda</h3>
<span class="text-lg leading-none text-zinc-500">×</span>
</div>
<div class="max-h-64 overflow-y-auto px-5 py-4">
<p class="text-sm text-zinc-400">
Escolha o que importar e o que publicar no feed da agenda. As opções continuam
acessíveis rolando dentro do modal.
</p>
<div class="mt-4 space-y-2">
<div class="flex items-start gap-3 rounded-lg border border-zinc-800 p-3">
<span class="mt-0.5 h-4 w-4 shrink-0 rounded border border-zinc-600"></span>
<div class="min-w-0 flex-1">
<p class="text-sm text-zinc-300">Importar eventos do Google</p>
<div class="mt-1.5 h-3 w-64 rounded bg-zinc-800"></div>
</div>
</div>
<div class="flex items-start gap-3 rounded-lg border border-zinc-800 p-3">
<span class="mt-0.5 h-4 w-4 shrink-0 rounded border border-zinc-600"></span>
<div class="min-w-0 flex-1">
<p class="text-sm text-zinc-300">Publicar atividades no Google Agenda</p>
<div class="mt-1.5 h-3 w-56 rounded bg-zinc-800"></div>
</div>
</div>
<div class="flex items-start gap-3 rounded-lg border border-zinc-800 p-3">
<span class="mt-0.5 h-4 w-4 shrink-0 rounded border border-zinc-600"></span>
<div class="min-w-0 flex-1">
<p class="text-sm text-zinc-300">Selecionar quais calendários sincronizar</p>
<div class="mt-1.5 h-3 w-72 rounded bg-zinc-800"></div>
</div>
</div>
<div class="flex items-start gap-3 rounded-lg border border-zinc-800 p-3">
<span class="mt-0.5 h-4 w-4 shrink-0 rounded border border-zinc-600"></span>
<div class="min-w-0 flex-1">
<p class="text-sm text-zinc-300">Avisos de conflito de horário</p>
<div class="mt-1.5 h-3 w-48 rounded bg-zinc-800"></div>
</div>
</div>
<div class="flex items-start gap-3 rounded-lg border border-zinc-800 p-3">
<span class="mt-0.5 h-4 w-4 shrink-0 rounded border border-zinc-600"></span>
<div class="min-w-0 flex-1">
<p class="text-sm text-zinc-300">Fuso horário dos eventos</p>
<div class="mt-1.5 h-3 w-40 rounded bg-zinc-800"></div>
</div>
</div>
<div class="flex items-start gap-3 rounded-lg border border-zinc-800 p-3">
<span class="mt-0.5 h-4 w-4 shrink-0 rounded border border-zinc-600"></span>
<div class="min-w-0 flex-1">
<p class="text-sm text-zinc-300">Reprocessar sincronização anterior</p>
<div class="mt-1.5 h-3 w-60 rounded bg-zinc-800"></div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 border-t border-zinc-800 px-5 py-4">
<div
class="flex h-11 items-center rounded-lg border border-zinc-700 px-4 text-sm font-medium text-zinc-200"
>
Fechar
</div>
<div
class="flex h-11 items-center rounded-lg bg-indigo-600 px-4 text-sm font-medium text-white"
>
Sincronizar agora
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Cena 3 · mobile ~390px · o mesmo modal de criação (não muda) -->
<section class="mt-10">
<p class="text-xs font-medium uppercase tracking-wider text-zinc-500">
Cena 3 · Mobile ~390px — o mesmo modal de criação (não muda)
</p>
<p class="mt-1 text-xs text-zinc-500">
Hoje o mobile já abre como folha (Drawer) com corpo rolável e campos empilhados; a
correção desktop não pode alterar isso.
</p>
<div class="mt-3 w-[390px] rounded-xl border border-zinc-800 bg-zinc-900/40 p-4">
<div class="space-y-2 opacity-40" aria-hidden="true">
<div class="h-3 w-40 rounded bg-zinc-800"></div>
<div class="h-3 w-24 rounded bg-zinc-800"></div>
</div>
<div
class="mt-4 overflow-hidden rounded-t-2xl border-x border-t border-zinc-700 bg-zinc-900"
>
<div class="mx-auto mt-3 h-1 w-10 rounded-full bg-zinc-700"></div>
<div class="flex items-center justify-between px-4 pb-2 pt-3">
<h3 class="text-sm font-semibold">Nova atividade</h3>
<span class="text-lg leading-none text-zinc-500">×</span>
</div>
<div class="max-h-72 overflow-y-auto px-4 pb-4">
<div class="grid gap-4">
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Título</span>
<div
class="flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm text-zinc-600"
>
Título da atividade
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Tipo</span>
<div
class="flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm"
>
<span class="text-zinc-600">Selecione o tipo</span
><span class="text-zinc-600">▾</span>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Município</span>
<div
class="flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm"
>
<span class="text-zinc-600">Selecione o município</span
><span class="text-zinc-600">▾</span>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Início</span>
<div
class="flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm text-zinc-600"
>
dd/mm/aaaa
</div>
<div class="mt-2 flex gap-2">
<div
class="flex h-11 w-20 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">Hora</span><span class="text-zinc-600">▾</span>
</div>
<div
class="flex h-11 w-20 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">Min</span><span class="text-zinc-600">▾</span>
</div>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400">Término</span>
<div
class="flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 text-sm text-zinc-600"
>
dd/mm/aaaa
</div>
<div class="mt-2 flex gap-2">
<div
class="flex h-11 w-20 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">Hora</span><span class="text-zinc-600">▾</span>
</div>
<div
class="flex h-11 w-20 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950 px-2 text-sm"
>
<span class="text-zinc-600">Min</span><span class="text-zinc-600">▾</span>
</div>
</div>
</div>
<div>
<span class="mb-1.5 block text-xs font-medium text-zinc-400"
>Assessores responsáveis</span
>
<div class="flex flex-wrap gap-2">
<div class="h-9 w-28 rounded-lg bg-zinc-800"></div>
<div class="h-9 w-36 rounded-lg bg-zinc-800"></div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-3 border-t border-zinc-800 px-4 py-4">
<div
class="flex h-11 items-center justify-center rounded-lg border border-zinc-700 text-sm font-medium text-zinc-200"
>
Cancelar
</div>
<div
class="flex h-11 items-center justify-center rounded-lg bg-indigo-600 text-sm font-medium text-white"
>
Salvar
</div>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,93 @@
# Agenda: modais usáveis no desktop (criação de atividade, sync e feed)
Status: rascunho
Atualizado em: 2026-08-27
Issue: #933
Priority: P2
Model: composer-2.5
Impeccable: B — encaixe nos modais da agenda
Rascunho UI: docs/plans/c148-modais-agenda-desktop-ui-draft.html
Appetite: ~0,5–1 dia eng; um outcome verificável — no desktop, os três modais da agenda abrem com largura/padding coerentes, conteúdo inteiro acessível (scroll quando precisa) e o par data/hora não empilha
Responsável: —
## Intenção
Dois pedidos fundidos sobre `/campanha/atividades` no desktop. No modal de criação de atividade: "o layout no desktop está esquisito — bordas gigantes, modal desnecessariamente estreito, e o seletor de data/hora fica um em cima do outro". No modal de sincronização Google: "o modal é estreito demais e o conteúdo passa do topo e da base da tela, sem rolagem para ver o resto". É a mesma dor — modais da agenda que não respeitam a janela do desktop — e o modal de feed ICS carrega o mesmo defeito latente do sync. Correção de encaixe, não redesign.
## Persona e fluxo
- **Persona / contexto:** assessor (ou coordenador) na mesa, no desktop, montando a agenda da semana e sincronizando o Google Calendar; tarefa operacional curta e repetida.
- **Job principal:** criar/editar uma atividade e configurar a sincronização sem lutar contra o modal.
- **Fluxo desejado:**
1. Abre "Nova atividade" → modal abre com largura generosa e paddings contidos (nada de "bordas gigantes").
2. Em Início/Término, os dois campos ficam lado a lado, cada um com data e hora na mesma linha.
3. Se o conteúdo excede a altura da janela, o corpo rola internamente — os botões de ação ficam sempre visíveis no rodapé.
4. Abre o modal de sync Google → a lista de opções rola no corpo, footer fixo.
5. Abre o modal de feed ICS → mesmo comportamento de corpo rolável.
6. No mobile, nada muda (folha com rolagem de hoje).
- **Anti-goals de produto:** redesign de formulário/campos, mudança de lógica (validação/salvamento) dos modais, padronização de outros modais do app, feature nova.
### Esboço de fluxo (B/C/D)
```text
[/campanha/atividades] → [Nova atividade · modal desktop] → preencher → Início/Término lado a lado → [Salvar]
→ [Sync Google · modal] → corpo com rolagem + footer fixo → [Sincronizar]
→ [Feed ICS · modal] → corpo com rolagem + footer fixo → [Copiar link]
```
### Rascunho UI (B/C/D)
- Rascunho UI (gate): `docs/plans/c148-modais-agenda-desktop-ui-draft.html` — cena 1 (criação em desktop ~1280px), cena 2 (sync Google com corpo rolável e footer fixo; feed ICS segue o mesmo padrão), cena 3 (a mesma criação em ~390px, mobile inalterado).
## Objetivo e aceite
- No desktop, os TRÊS modais da agenda (criação/edição de atividade, sync Google, feed ICS) abrem com largura e padding coerentes — sem bordas gigantes, sem largura espremida.
- Nenhum dos três corta conteúdo: quando o corpo excede a altura disponível, há rolagem interna e o rodapé de ações permanece visível.
- O par Início/Término do modal de criação não empilha no desktop — data e hora de cada campo na mesma linha (o rascunho mostra a disposição recomendada).
- Guardrails: mobile dos três modais intocado (apenas não pode regredir); lógica de validação/salvamento e dados intocados; nenhum outro modal do app muda.
## Dados (intenção)
- **Vou apresentar dados?** Não — correção de layout/encaixe; nenhuma superfície de dados nova.
- **Decisões desbloqueadas:** nenhuma além do aceite visual do gate.
## Dados da decisão (literais)
- N/A — a intenção não fixa IDs, strings ou thresholds; os literais operacionais são os três modais nomeados na direção e o corte desktop vs mobile (mobile = comportamento atual da folha com rolagem).
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/components/campaign/activity/` — `ActivityOverlay.tsx` (criação/edição), `GoogleCalendarSyncDialog.tsx`, `CalendarFeedDialog.tsx`, `ActivityDateTimeField.tsx`; base compartilhada `src/components/ui/dialog.tsx`.
- **Hipótese central:** o override desktop do overlay usa largura/padding não-responsivos (`max-w-2xl`, `p-0`) que NÃO derrubam as variantes `sm:` da base (`sm:max-w-md`, `sm:p-8`) — em telas ≥ sm vencem as da base, e o modal fica com 28rem de largura e 2rem de padding (o "estreito + bordas gigantes"). Nos dois dialogs menores falta `max-h` + rolagem no caminho desktop (o mobile já resolve com a folha). O empilhamento data/hora vem da caixa estreita + grade de duas colunas.
- **Precedente a olhar:** o padrão mobile do próprio overlay (folha com max-h e corpo rolável) e do sync/feed no mobile — é o padrão a replicar no desktop.
- **Risco de acoplamento:** `ui/dialog.tsx` é a base de todos os modais do app — mudá-la globalmente causaria regressão silenciosa em toda tela; preferir correção local nos três modais da agenda (override explícito por breakpoint), sem tocar em outros consumidores.
## Dependências
- Nenhuma dura. Soft: C123 (ActivityOverlay — superfície de criação atual) e C94 (feed ICS) como precedentes de onde o defeito vive.
## Fora de escopo
- Mobile dos três modais (já está bom) — apenas garantir que não regrida.
- Lógica dos modais: validação, salvamento, dados e estados de erro intocados.
- Redesenho do formulário de atividade (campos, defaults, seções) — outro dia, outro pedido.
- Padronização de outros modais do app fora da agenda.
- A funcionalidade de sync/feed em si (feature existente — só o encaixe).
## Rabbit holes de produto
- **"Aproveitar e padronizar todos os modais do app."** Se alguém "só completar": varredura de todas as telas, PR gigante, regressão em massa. **Corte neste item:** só os três modais da agenda.
- **"Redesenhar o formulário de atividade."** Se alguém "só completar": reorganizar seções, campos novos, defaults. **Corte:** só encaixe/medidas dos campos existentes.
- **"Mudar a base ui/dialog.tsx para resolver de uma vez."** Se alguém "só completar": mudança global sem varredura de consumidores. **Corte:** mudança local nos três modais; a base só muda se o executor varrer os consumidores e provar zero regressão.
## Questões em aberto (produto)
- **Como dispor Início/Término no desktop?** **Opções:** A) os dois campos lado a lado, cada um com data+hora inline | B) um campo por linha com data+hora na mesma linha. **Recomendação:** A no desktop (aproveita a largura recuperada), fallback B se apertado. _(assumido — validar no gate com o rascunho; a cena 1 mostra A)._
- **Largura dos modais de sync/feed:** manter a atual (~32rem) ou ampliar um pouco (~36rem)? **Recomendação:** ampliar pouco e garantir rolagem — o defeito principal do sync/feed é a falta de scroll, não a largura. _(assumido — a cena 2 mostra ~36rem)._
## Referências
- GitHub Issue: — (após `pnpm agent:register`)
- Rascunho UI (gate): `docs/plans/c148-modais-agenda-desktop-ui-draft.html`
- Arquivos para abrir primeiro: `src/components/campaign/activity/ActivityOverlay.tsx`, `GoogleCalendarSyncDialog.tsx`, `CalendarFeedDialog.tsx`, `ActivityDateTimeField.tsx`, `src/components/ui/dialog.tsx`
- `AGENTS-campaign.md` — seção "Campaign activities (C3)" (vocabulário Atividade; dado interno de staff, sem LGPD/consent aqui)

View file

@ -0,0 +1,178 @@
<!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 — C149 Conectar Google (OAuth)</title>
</head>
<body class="bg-zinc-950 text-zinc-100 antialiased">
<main class="mx-auto max-w-5xl p-6 md:p-8">
<p class="text-sm text-zinc-400">
Candidato/coordenador configura o espelho Google Calendar da agenda — conexão por botão, sem
service account. Cenas: não configurado, conectado, erro de token, mobile.
</p>
<section class="mt-8">
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">
Cena 1 · Desktop — estado: não configurado
</p>
<div class="mt-3 rounded-lg border border-zinc-800 bg-zinc-900 p-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 class="text-lg font-semibold">Google Calendar</h2>
<p class="mt-1 text-sm text-zinc-400">
Espelha a agenda da campanha num calendário Google, com atualização em minutos.
</p>
</div>
<span
class="self-start rounded-full border border-zinc-700 px-3 py-1 text-xs text-zinc-300"
>
Não configurado
</span>
</div>
<div class="mt-6 rounded-md border border-dashed border-zinc-700 p-6">
<p class="text-sm text-zinc-300">
Conecte a conta Google do calendário da campanha para ativar o espelho.
</p>
<p class="mt-1 text-sm text-zinc-500">
Você será levado à tela de consentimento do Google para autorizar o Teqo a gerenciar o
calendário da campanha.
</p>
<button class="mt-4 h-11 rounded-lg bg-sky-600 px-5 text-sm font-medium text-white">
Conectar com o Google
</button>
</div>
<p class="mt-4 text-xs text-zinc-500">
A configuração manual por service account continua disponível como caminho técnico de
fallback.
</p>
</div>
</section>
<section class="mt-8">
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">
Cena 2 · Desktop — estado: conectado
</p>
<div class="mt-3 rounded-lg border border-zinc-800 bg-zinc-900 p-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 class="text-lg font-semibold">Google Calendar</h2>
<p class="mt-1 text-sm text-zinc-400">
O espelho usa a conexão abaixo para escrever os compromissos no calendário da
campanha.
</p>
</div>
<span
class="self-start rounded-full border border-zinc-700 px-3 py-1 text-xs text-zinc-300"
>
Conectado
</span>
</div>
<div
class="mt-6 flex flex-col gap-4 rounded-md border border-zinc-800 p-4 sm:flex-row sm:items-center sm:justify-between"
>
<div class="flex items-center gap-3">
<div class="h-9 w-9 rounded-full bg-zinc-800"></div>
<div>
<p class="text-sm font-medium">Conta Google da campanha</p>
<div class="mt-1 h-3 w-40 rounded bg-zinc-800"></div>
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<button class="h-10 rounded-lg border border-zinc-700 px-4 text-sm text-zinc-200">
Regerar acesso
</button>
<button class="h-10 rounded-lg border border-zinc-700 px-4 text-sm text-zinc-200">
Desconectar
</button>
</div>
</div>
<p class="mt-4 text-xs text-zinc-500">
Desconectar revoga o acesso do Teqo; depois, revogue o app nas configurações de
segurança da conta Google.
</p>
</div>
</section>
<section class="mt-8">
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">
Cena 3 · Desktop — estado: erro de token (caminho de reconexão)
</p>
<div class="mt-3 rounded-lg border border-zinc-800 bg-zinc-900 p-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 class="text-lg font-semibold">Google Calendar</h2>
<p class="mt-1 text-sm text-zinc-400">
O espelho está pausado até a conexão ser restabelecida.
</p>
</div>
<span
class="self-start rounded-full border border-zinc-700 px-3 py-1 text-xs text-zinc-300"
>
Erro
</span>
</div>
<div class="mt-6 rounded-md border border-zinc-700 p-6">
<p class="text-sm font-medium text-zinc-100">Erro de conexão com o Google</p>
<p class="mt-1 text-sm text-zinc-400">
O token de acesso não pôde ser renovado. O Teqo continua sendo a fonte da verdade — a
agenda segue funcionando aqui; nada foi perdido no espelho.
</p>
<p class="mt-3 text-sm text-zinc-500">
Reconecte com a mesma conta Google do calendário da campanha. Se a conta mudou,
desconecte antes e conecte novamente.
</p>
<div class="mt-4 flex flex-wrap items-center gap-3">
<button class="h-11 rounded-lg bg-sky-600 px-5 text-sm font-medium text-white">
Reconectar com o Google
</button>
<button class="h-11 rounded-lg border border-zinc-700 px-4 text-sm text-zinc-200">
Desconectar
</button>
</div>
</div>
</div>
</section>
<section class="mt-8">
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">
Cena 4 · Mobile ~390px — estado: não configurado (cena 1)
</p>
<div class="mt-3 w-[390px] max-w-full">
<div class="rounded-lg border border-zinc-800 bg-zinc-900 p-4">
<div class="flex flex-col gap-3">
<div>
<h2 class="text-base font-semibold">Google Calendar</h2>
<p class="mt-1 text-sm text-zinc-400">
Espelha a agenda da campanha num calendário Google.
</p>
</div>
<span
class="self-start rounded-full border border-zinc-700 px-3 py-1 text-xs text-zinc-300"
>
Não configurado
</span>
</div>
<div class="mt-4 rounded-md border border-dashed border-zinc-700 p-4">
<p class="text-sm text-zinc-300">
Conecte a conta Google do calendário da campanha para ativar o espelho.
</p>
<p class="mt-1 text-sm text-zinc-500">
Você autoriza o Teqo na tela de consentimento do Google.
</p>
<button
class="mt-4 h-11 w-full rounded-lg bg-sky-600 px-5 text-sm font-medium text-white"
>
Conectar com o Google
</button>
</div>
<p class="mt-3 text-xs text-zinc-500">
Service account segue disponível como fallback técnico.
</p>
</div>
</div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,91 @@
# C149 — Google Calendar: botão Conectar Google (OAuth) substitui a configuração manual de service account
Status: rascunho
Atualizado em: 2026-08-27
Issue: #934
Priority: P2
Model: composer-2.5
Depends: #932 (C147)
Impeccable: B — encaixe na agenda (configuração do sync)
Rascunho UI: docs/plans/c149-conectar-google-oauth-ui-draft.html
Appetite: ~2 dias eng; um outcome verificável — o candidato/coordenador conecta a conta Google do calendário da campanha clicando um botão (consent do Google), sem criar service account nem colar JSON
Responsável: —
## Intenção
Ativar o espelho Google Calendar (C114) hoje exige cerimônia de engenharia: criar service account no GCP, baixar JSON, base64, colar em env, compartilhar o calendário com o e-mail da conta de serviço. O dono do produto: _"This manual configuration of the service account plus calendar isn't ideal — isn't there a way to expose a button that allows the candidate to sync a calendar to it?"_ O registro C114 recusou OAuth **por pessoa** (N staff escrevendo no calendário pessoal de cada um). O pedido atual é diferente: **uma** concessão OAuth em que candidato/coordenador autoriza o Teqo a gerenciar **o calendário da campanha** — variante não coberta pelo registro, vinda como nova evidência do dono. Configuração vira um clique.
## Persona e fluxo
- **Persona / contexto:** candidato ou coordenador geral, no computador (mesa), configurando o espelho da agenda uma única vez — e voltando só quando algo quebra.
- **Job principal:** "conecto a conta Google do calendário da campanha ao Teqo sem ajuda de engenharia".
- **Fluxo desejado:** na configuração do sync da agenda → clica "Conectar com o Google" → tela de consent do Google (escolhe a conta da campanha) → volta ao Teqo com estado "conectado" → o sync passa a usar essa conexão; se o token quebrar um dia, o estado mostra "erro" com caminho de reconexão (clicar de novo).
- **Anti-goals de produto:** não é OAuth por pessoa (C114 continua valendo — UMA concessão, calendário da campanha); não é migração obrigatória (service account segue funcionando como fallback); não mexe na seleção de qual calendário (C150) nem no motor de reconciliação; leader não vê nem conecta (lockdown).
### Esboço de fluxo (B)
```text
[/campanha/agenda — configuração do sync] → "Conectar com o Google" → consent Google (conta da campanha)
→ callback de volta ao Teqo → estado "conectado" → sync usa a conexão (service account vira fallback)
```
### Rascunho UI (gate)
- Rascunho UI (gate): `docs/plans/c149-conectar-google-oauth-ui-draft.html` — cenas: não configurado (botão), conectado, erro de token com reconexão, mobile.
## Objetivo e aceite
- O candidato/coordenador conecta a conta Google do calendário da campanha clicando um botão (consent do Google) — sem criar service account, sem colar JSON, sem tocar em GCP.
- Estado da conexão visível e honesto: conectado | erro | não configurado; erro de token mostra caminho de reconexão em um clique.
- Falha de token/Google não corrompe dados: Teqo continua SoT; o espelho degrada para "erro", nada é apagado nem duplicado.
- Funciona atrás do túnel Cloudflare: o callback OAuth usa a URL pública correta (nada de localhost vazando no redirect).
- Guardrails: o token nunca aparece em log; armazenamento fail-closed (sem conexão → sem sync, sem crash); desconectar revoga o acesso no Teqo e orienta revogar no Google; leader lockdown intocado.
## Dados (intenção)
- **Vou apresentar dados?** Não — a conexão é infraestrutura do espelho (saída); nenhuma leitura nova de dados no Teqo. O `calendarList` (escopo readonly) alimenta apenas a escolha de calendário do C150.
- **Decisões desbloqueadas:** candidato/coordenador decide (uma vez) qual conta Google autoriza; diante de "erro", o mesmo ator decide reconectar ou desconectar.
- **Forma:** _adiada ao plano de implementação_ — aqui só as restrições: estado ternário, nunca exibir o token, sem inventar estados intermediários.
## Dados da decisão (literais)
- Escopos OAuth mínimos: `https://www.googleapis.com/auth/calendar.events` + `https://www.googleapis.com/auth/calendar.calendarlist.readonly` (o segundo basta para listar calendários no C150 — sem escopo além do necessário).
- Estados da conexão (UI + registro): `conectado | erro | não configurado` — exatamente esses três.
- Env de credencial que vira fallback (não renomear nem remover nesta entrega): `GOOGLE_CALENDAR_SERVICE_ACCOUNT_KEY`.
- Restrição dura de produto: o token DEVE sobreviver sem re-consentimento semanal (o caminho técnico — app unverified vs verificação Google — fica em aberto, ver Questões).
## Direção no codebase (hipótese)
- **Áreas prováveis:** configuração do sync na agenda `/campanha/agenda` (botão/estado), rotas de callback OAuth em `src/app/(campaign)/campanha/…`, utilitários Google perto de `googleCalendarSync.ts`/`googleCalendarClient.ts`, armazenamento da conexão na collection `googleCalendarSync` (onde `calendarId` já vive).
- **Precedente a olhar:** plano C114 (decisão "não OAuth por pessoa"); par `readGoogleServiceAccountCredentials`/`googleCalendarClient` (REST hand-rolled com `jose`, sem lib Google hoje); WebAuthn de campaignUser (precedente de cerimônia de credencial com rota fora de `(app)`).
- **Risco de acoplamento:** o token é credencial da conta Google da campanha — nunca logar, fail-closed, desconectar revoga; leader lockdown; C147 merged antes (o sync funcionando/diagnosticado) para não brigar no mesmo client.
## Dependências
- **Dura: C147** — o sync precisa estar funcionando/diagnosticado antes de trocar o modelo de credencial (também evita conflito de arquivos no client).
## Fora de escopo
- Escolha/listagem de qual calendário conectar → **C150** (aqui só entrega a conexão e o estado).
- Migração/remoção obrigatória da service account (fica como fallback; aposentar é entrega futura).
- OAuth por pessoa (anti-goal C114, reafirmado) e leitura de agenda pessoal para conflito de horário (item separado, se um dia existir).
- Motor de reconciliação do sync (janela, locks, hooks C114/C122) — intocado.
## Rabbit holes de produto
- **"Verificação completa do app no Google."** Se alguém "só completar": auditoria, casework, semanas de fila — para uso interno. **Corte neste item:** caminho unverified (uso interno, <100 usuários) com aviso no consent; verificação só se o produto um dia precisar.
- **"Desconectar + limpar tudo + migrar quem usa service account."** **Corte:** desconectar = revogar no Teqo + orientação de revogar no Google; ninguém é obrigado a migrar nesta entrega.
- **"Seletor de calendário dentro do botão."** Se alguém "só completar": puxa o C150 para dentro. **Corte:** a conexão autoriza; escolher o calendário é o item seguinte.
## Questões em aberto (produto)
- **Manter a service account como fallback depois que o OAuth existir?** **Opções:** A) sim, como caminho técnico até o OAuth estar comprovado em produção | B) aposentar na mesma entrega. **Recomendação:** **A** — aposentar é entrega separada. _(assumido — validar com produto)_
- **Quem pode conectar/desconectar?** **Opções:** A) candidato + coordenador | B) só candidato | C) também advisor. **Recomendação:** **A** — é a credencial da campanha inteira; advisor não autoriza credencial global, leader lockdown. _(assumido — validar)_
- **App unverified ou verificação Google?** **Opções:** A) unverified (aviso no consent, token não expira em ~7 dias) | B) verificação completa (sem aviso, custo de auditoria). **Recomendação:** **A** — uso interno, <100 usuários; a restrição dura é o token sobreviver sem re-consentimento semanal. _(trade-off do aviso é a decisão de produto)_
## Referências
- Rascunho UI (gate): `docs/plans/c149-conectar-google-oauth-ui-draft.html`
- Plan C114 (`c114-google-calendar-push-agenda-oficial.md` — decisão "não OAuth por pessoa"); C147 (dependência dura); C150 (seleção de calendário, fora daqui)
- `src/utilities/googleCalendarSync.ts`, `src/utilities/googleCalendarClient.ts`, collection `googleCalendarSync` (`calendarId`)
- `AGENTS-campaign.md` — leader lockdown / papéis de `/campanha`

View file

@ -0,0 +1,192 @@
<!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 — C150 Calendário principal e inscrição de staff no Google</title>
</head>
<body class="bg-zinc-950 text-zinc-100 antialiased">
<main class="mx-auto max-w-5xl p-8">
<p class="text-sm text-zinc-400">
C150 — Agenda do Teqo: o núcleo (candidato/coordenador) escolhe o calendário principal da
campanha; o staff adiciona a agenda ao Google dele com um clique. Líder nunca vê estas
ações.
</p>
<section class="mt-10">
<p class="text-xs uppercase tracking-wide text-zinc-500">
Cena 1 — desktop · seletor de calendário principal (candidato/coordenador)
</p>
<div class="mt-3 rounded-xl border border-zinc-800 bg-zinc-900 p-6">
<div class="mx-auto max-w-md rounded-lg border border-zinc-700 bg-zinc-950">
<div class="border-b border-zinc-800 p-4">
<h2 class="text-base font-medium">Calendário principal da campanha</h2>
<p class="mt-1 text-sm text-zinc-400">
Criar e editar atividades espelham no calendário escolhido.
</p>
</div>
<div class="p-4">
<p class="text-xs text-zinc-500">Calendários da conta Google conectada</p>
<ul class="mt-3 space-y-3">
<li class="flex items-center gap-3 rounded-lg border border-indigo-600 p-3">
<span
class="flex h-4 w-4 items-center justify-center rounded-full border border-indigo-500"
>
<span class="h-2 w-2 rounded-full bg-indigo-500"></span>
</span>
<span class="text-sm">Calendário da campanha</span>
<span class="ml-auto text-xs text-zinc-500">em uso</span>
</li>
<li class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="h-4 w-4 rounded-full border border-zinc-600"></span>
<span class="text-sm">Meu calendário</span>
</li>
<li class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="h-4 w-4 rounded-full border border-zinc-600"></span>
<span class="inline-block h-3 w-28 rounded bg-zinc-700/60"></span>
</li>
</ul>
</div>
<div class="flex items-center justify-end gap-3 border-t border-zinc-800 p-4">
<span
class="inline-flex h-11 items-center rounded-lg border border-zinc-700 px-4 text-sm text-zinc-300"
>Cancelar</span
>
<span
class="inline-flex h-11 items-center rounded-lg bg-indigo-600 px-4 text-sm font-medium text-white"
>Escolher calendário</span
>
</div>
</div>
</div>
</section>
<section class="mt-10">
<p class="text-xs uppercase tracking-wide text-zinc-500">
Estado — desktop · sem conta Google conectada (depende de C149)
</p>
<div class="mt-3 rounded-xl border border-zinc-800 bg-zinc-900 p-6">
<div class="mx-auto max-w-md rounded-lg border border-zinc-700 bg-zinc-950 p-4">
<h2 class="text-base font-medium">Calendário principal da campanha</h2>
<p class="mt-1 text-sm text-zinc-400">
Conecte a conta Google da campanha para listar e escolher o calendário principal.
</p>
<div class="mt-4 flex justify-end">
<span
class="inline-flex h-11 items-center rounded-lg bg-indigo-600 px-4 text-sm font-medium text-white"
>Conectar conta Google</span
>
</div>
</div>
</div>
</section>
<section class="mt-10">
<p class="text-xs uppercase tracking-wide text-zinc-500">
Cena 2 — desktop · chrome da agenda com o botão de staff
</p>
<div class="mt-3 rounded-xl border border-zinc-800 bg-zinc-900 p-6">
<div class="rounded-lg border border-zinc-800 bg-zinc-950">
<div class="flex flex-wrap items-center gap-3 border-b border-zinc-800 p-4">
<h2 class="text-base font-medium">Agenda</h2>
<span
class="inline-flex h-9 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-300"
>Agosto de 2026</span
>
<span
class="ml-auto inline-flex h-9 items-center rounded-lg border border-zinc-800 px-3 text-sm text-zinc-400"
>Google: sincronizado</span
>
<span
class="inline-flex h-9 items-center rounded-lg border border-zinc-700 px-3 text-sm text-zinc-300"
>Link de import</span
>
<span
class="inline-flex h-11 items-center rounded-lg bg-indigo-600 px-4 text-sm font-medium text-white"
>Adicionar ao meu Google Calendar</span
>
</div>
<div class="space-y-3 p-4">
<div class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="inline-block h-3 w-40 rounded bg-zinc-700/60"></span>
<span class="ml-auto text-xs text-zinc-500">município</span>
</div>
<div class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="inline-block h-3 w-28 rounded bg-zinc-700/40"></span>
<span class="ml-auto text-xs text-zinc-500">município</span>
</div>
</div>
</div>
<p class="mt-3 text-xs text-zinc-500">
O recorte filtrado (município/tags) segue no fluxo atual do feed ICS: copiar link +
instrução "Do URL" — botão "Link de import".
</p>
</div>
</section>
<section class="mt-10">
<p class="text-xs uppercase tracking-wide text-zinc-500">
Estado — desktop · confirmação da inscrição
</p>
<div class="mt-3 rounded-xl border border-zinc-800 bg-zinc-900 p-6">
<div class="rounded-lg border border-zinc-800 bg-zinc-950">
<div class="border-b border-zinc-800 p-4">
<p class="text-sm">Agenda da campanha adicionada ao seu Google Calendar.</p>
<p class="mt-1 text-sm text-zinc-400">
Eventos e edições aparecem em minutos, conforme as notificações do seu Google.
</p>
</div>
<div class="space-y-3 p-4">
<div class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="inline-block h-3 w-40 rounded bg-zinc-700/60"></span>
<span class="ml-auto text-xs text-zinc-500">município</span>
</div>
<div class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="inline-block h-3 w-28 rounded bg-zinc-700/40"></span>
<span class="ml-auto text-xs text-zinc-500">município</span>
</div>
</div>
</div>
</div>
</section>
<section class="mt-10">
<p class="text-xs uppercase tracking-wide text-zinc-500">
Cena 3 — mobile 390px · botão de staff (caso de uso em campo)
</p>
<div class="mt-3 rounded-xl border border-zinc-800 bg-zinc-900 p-6">
<div class="w-[390px] rounded-lg border border-zinc-800 bg-zinc-950">
<div class="border-b border-zinc-800 p-4">
<div class="flex items-center justify-between gap-3">
<h2 class="text-base font-medium">Agenda</h2>
<span
class="inline-flex h-9 items-center rounded-lg border border-zinc-800 px-3 text-sm text-zinc-400"
>Google: sincronizado</span
>
</div>
<div class="mt-3 space-y-3">
<span
class="flex h-11 items-center justify-center rounded-lg bg-indigo-600 text-sm font-medium text-white"
>Adicionar ao meu Google Calendar</span
>
<span
class="flex h-11 items-center justify-center rounded-lg border border-zinc-700 text-sm text-zinc-300"
>Link de import</span
>
</div>
</div>
<div class="space-y-3 p-4">
<div class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="inline-block h-3 w-32 rounded bg-zinc-700/60"></span>
</div>
<div class="flex items-center gap-3 rounded-lg border border-zinc-800 p-3">
<span class="inline-block h-3 w-24 rounded bg-zinc-700/40"></span>
</div>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,100 @@
# C150 — Agenda: calendário principal escolhido pelo candidato/coordenador + inscrição de staff no Google
Status: rascunho
Atualizado em: 2026-08-27
Issue: #935
Priority: P2
Model: composer-2.5
Depends: #934 (C149)
Impeccable: B — encaixe na agenda
Rascunho UI: docs/plans/c150-calendario-principal-e-inscricao-staff-ui-draft.html
Appetite: ~1,5–2 dias eng; um outcome verificável — o candidato/coordenador escolhe o calendário principal da campanha na própria agenda (create/edit espelham nele) e um staff adiciona a agenda ao Google dele com um clique
Responsável: —
## Intenção
Dois pedidos fundidos sobre o espelho Google Calendar entregue por C114/C115. Hoje, escolher QUAL calendário Google espelha a agenda exige o admin do Payload — mas quem decide o calendário oficial da campanha é o candidato/coordenador. E inscrever a agenda no Google pessoal de cada staff ainda é manual: copiar o link e configurar "Por URL" à mão — o próprio pedido reconhece que o caminho certo é um botão que leva ao Google já preparado. Na agenda do Teqo: o núcleo escolhe o calendário principal entre os da conta Google conectada (C149) e o staff adiciona a agenda ao Google dele com um clique.
## Persona e fluxo
- **Persona / contexto:** candidato/coordenador na mesa decidindo a agenda oficial; assessores e comunicação em campo, vivendo no Google Calendar do celular; líder de campo nunca toca nisso.
- **Job principal:** "escolho qual calendário da nossa conta Google é a agenda oficial — e a agenda da campanha aparece no meu Google Calendar sem eu configurar nada à mão".
- **Fluxo desejado:**
1. Na agenda (`/campanha/agenda`), o candidato/coordenador abre a configuração do Google e vê os calendários da conta conectada.
2. Escolhe um como calendário principal e confirma; criar/editar atividades passam a espelhar nele.
3. Um staff (coordenador/assessor/candidato) clica "Adicionar ao meu Google Calendar": o Google abre com o calendário oficial pré-configurado — sem copiar link nem "Por URL".
4. Para recorte filtrado (município/tags), o caminho continua o feed ICS atual: copiar link + instrução — nada muda de comportamento.
- **Anti-goals de produto:** não escrever no calendário PESSOAL de cada staff (decisão C114); não criar N calendários espelhados — o calendário principal é UM; não refazer o feed ICS nem o motor de sync; sem import de eventos do Google além do que C115 já faz; leader lockdown intocado.
### Esboço de fluxo (B)
```text
[agenda /campanha/agenda] → "Calendário principal" (candidato/coordenador)
→ lista os calendários da conta Google conectada (C149) → escolhe → confirma
→ espelho re-concilia no calendário escolhido; create/edit espelham nele
[mesma agenda, staff] → "Adicionar ao meu Google Calendar" → um clique
→ Google abre com o calendário oficial pré-configurado → agenda no Google pessoal
```
### Rascunho UI (B)
- Rascunho UI (gate): `docs/plans/c150-calendario-principal-e-inscricao-staff-ui-draft.html`
## Objetivo e aceite
- Na própria agenda, o candidato/coordenador lista os calendários da conta Google conectada e escolhe o calendário principal — configurar o espelho deixa de exigir o admin do Payload.
- Criar/editar atividades espelham no calendário escolhido — o motor C114/C115 não muda de modelo; trocar o calendário re-concilia o espelho sem eventos órfãos/duplicados visíveis ao staff.
- Um staff adiciona a agenda ao Google pessoal com um clique — sem copiar link nem configurar "Por URL" à mão; o recorte filtrado segue no fluxo atual do feed ICS (link + instrução).
- Guardrails: leader não vê nem usa nenhuma das duas ações; Teqo continua SoT; o calendário principal é UM; falha do Google não derruba a agenda do Teqo (estado do sync continua visível); o admin do Payload continua como escape hatch da configuração.
## Dados (intenção)
- **Vou apresentar dados?** Não — o espelho é saída (mesma posição do C114); nenhuma leitura nova no Teqo.
- **Decisões desbloqueadas:** candidato/coordenador escolhe o calendário principal da campanha (ator + escolha, hoje exclusiva do admin do Payload); staff opta por inscrever a agenda no Google pessoal dele (ator + escolha, hoje manual).
- **Forma:** _adiada ao plano de implementação_ — a apresentação do evento no Google já é decisão do C114.
## Dados da decisão (literais)
- Padrão do link one-click do calendário oficial: `https://calendar.google.com/calendar/r?cid=<calendarId URL-encoded>` — confiável para calendar IDs de calendários Google reais (caso do calendário oficial da campanha). O builder atual compõe o `cid` com a URL webcal pública do calendário — reconciliar com este literal na implementação.
- Para o recorte ICS, quando um one-click for tentado: `cid` com `webcal://<url-do-feed>` — INFIEL/instável para feeds ICS externos ("Unable to add calendar", eventos não populam); fallback obrigatório = copiar link + instrução "Do URL" (fluxo atual). Assumido v1: NÃO tentar — só link + instrução.
- Rótulos de placeholder do rascunho UI: "Calendário da campanha" (oficial) e "Meu calendário" (pessoal genérico da conta conectada).
## Direção no codebase (hipótese)
- **Áreas prováveis:** chrome da agenda em `src/components/campaign/activity/` (`AgendaGoogleSyncChrome.tsx`, `GoogleCalendarSyncDialog.tsx`, `CalendarFeedDialog.tsx`), ações em `src/app/(campaign)/campanha/actions/googleCalendarSync.ts`, papel em `src/utilities/access/googleCalendarSync.ts` (hoje `canSetGoogleCalendarSyncConfigField = isPayloadAdmin` — é isso que o produto quer abrir ao candidato/coordenador), links em `src/lib/googleCalendarLink.ts`.
- **Precedente a olhar:** plans C114/C115 (espelho e bidirecional — motor não muda de modelo), C16/C113 (feed ICS); trocar `calendarId` já dispara re-conciliação automática (config hook D7; snapshot pino por calendarId; canal push re-criado por calendário) — o delta é a troca de dono da escolha, não a mecânica.
- **Risco de acoplamento:** abrir a escolha a candidato/coordenador muda uma permissão de configuração hoje admin-only (a intenção do produto é exatamente essa); leader lockdown intocado; o admin do Payload permanece funcionando.
## Dependências
- **Dura:** C149 — o seletor nasce da conexão OAuth (listar os calendários da conta conectada).
- Suaves: C114/C115 (motor do espelho, em produção), C16/C92/C98/C113 (feed ICS e link de import).
## Fora de escopo
- Escrever no calendário pessoal de cada staff via OAuth individual → anti-goal reafirmado da decisão C114.
- N calendários espelhados (um por filtro/papel) → recorte continua no feed ICS (C16/C113).
- Refazer o feed ICS, o motor de sync ou o import bidirecional do C115.
- One-click universal para feeds ICS externos → infiável (pesquisa); o recorte mantém link + instrução.
- Escolher calendário principal por assessor → decisão de núcleo; assessor só inscreve.
## Rabbit holes de produto
- **"Um calendário principal por filtro/papel."** Se alguém "só completar": N espelhos divergentes e permissões por calendário. **Corte neste item:** um calendário principal, escolhido pelo núcleo; recorte continua no feed ICS.
- **"One-click para todo tipo de agenda (incl. ICS)."** Se alguém "só completar": botão que falha com "Unable to add calendar" e gera suporte. **Corte:** one-click só para o calendário oficial (Google real); recorte mantém copiar link + instrução.
- **"Sincronizar todo o histórico ao trocar de calendário."** **Corte:** o motor atual governa a janela; a troca re-usa a re-conciliação existente, sem backfill novo.
## Questões em aberto (produto)
- **O botão one-click do recorte filtrado (ICS) vale em v1?** **Opções:** A) sim, como tentativa com fallback claro | B) não — só o calendário oficial em v1, recorte segue link+instrução. **Recomendação:** B — o padrão cid é infiel para ICS e a falha custa confiança. _(assumido — validar no gate)_
- **A escolha do calendário principal substitui completamente a config no admin do Payload?** **Opções:** A) substitui na UI da agenda; admin fica como escape hatch | B) mantém as duas como superfícies de primeira classe. **Recomendação:** A — uma só casa para a decisão; admin permanece para operação/contingência. _(assumido — validar)_
- **Assessor pode escolher o calendário principal?** **Opções:** A) só candidato/coordenador | B) também assessor. **Recomendação:** A — decisão de núcleo sobre a agenda oficial; assessor apenas inscreve a agenda no Google dele.
## Referências
- GitHub Issue: — (após `pnpm agent:register`)
- Rascunho UI (gate): `docs/plans/c150-calendario-principal-e-inscricao-staff-ui-draft.html`
- Plans C114 (`c114-google-calendar-push-agenda-oficial.md`), C115 (`c115-google-calendar-edicao-bidirecional.md`), C16 (`sync-teqo-google-calendar.md`), C113 (`c113-feed-ical-agenda-nao-atualiza.md`)
- Arquivos para abrir primeiro: `src/lib/googleCalendarLink.ts`, `src/components/campaign/activity/AgendaGoogleSyncChrome.tsx`, `src/components/campaign/activity/GoogleCalendarSyncDialog.tsx`, `src/components/campaign/activity/CalendarFeedDialog.tsx`, `src/utilities/access/googleCalendarSync.ts`, `src/collections/GoogleCalendarSync.ts`, `src/utilities/activityUi.ts`
- `AGENTS-campaign.md` — convenção de papéis/lockdown tocada pelo item

View file

@ -0,0 +1,274 @@
<!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 — Atividade: marcar evento como público (C151)</title>
</head>
<body class="bg-zinc-950 text-zinc-100 antialiased">
<main class="mx-auto max-w-5xl p-6 sm:p-8">
<h1 class="text-lg font-medium text-zinc-100">
UI draft — Atividade: marcar evento como público (C151)
</h1>
<p class="mt-1 text-sm text-zinc-400">
Staff edita uma atividade no modal e marca "Evento público" (default desmarcado); a marcação
é durável — nenhuma tela muda de comportamento.
</p>
<p class="mt-2 text-sm text-zinc-500">
Cinza = contexto existente (sem mudança) · <span class="text-amber-400">acento</span> = novo
neste item. Posição do checkbox segue a recomendação (junto dos outros checkboxes) — validar
no gate.
</p>
<section class="mt-8">
<p class="text-xs uppercase tracking-wide text-zinc-500">
Cena 1 · desktop — modal de atividade, seções "Informações básicas" e "Agendamento"
</p>
<div
class="mx-auto mt-3 max-w-2xl overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900"
>
<div class="flex items-center justify-between border-b border-zinc-800 px-6 py-4">
<div>
<p class="text-sm font-medium text-zinc-200">Editar atividade</p>
<p class="text-xs text-zinc-500">/campanha/atividades</p>
</div>
<span class="text-lg text-zinc-500">×</span>
</div>
<div class="px-6 py-5">
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">
Informações básicas
</p>
<div class="mt-3 space-y-3">
<div>
<p class="text-xs text-zinc-400">Título</p>
<div
class="mt-1 flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-44 rounded bg-zinc-800"></span>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-xs text-zinc-400">Tipo</p>
<div
class="mt-1 flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-20 rounded bg-zinc-800"></span>
<span class="text-xs text-zinc-600">▾</span>
</div>
</div>
<div>
<p class="text-xs text-zinc-400">Status</p>
<div
class="mt-1 flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-20 rounded bg-zinc-800"></span>
<span class="text-xs text-zinc-600">▾</span>
</div>
</div>
</div>
<label class="flex items-start gap-3 pt-1">
<input type="checkbox" class="mt-0.5 h-4 w-4 accent-zinc-500" />
<span>
<span class="block text-sm text-zinc-300">Deputado presente</span>
<span class="block text-xs text-zinc-500"
>Marque quando o deputado estiver presente na atividade.</span
>
</span>
</label>
</div>
<p class="mt-6 text-xs font-medium uppercase tracking-wide text-zinc-500">
Agendamento
</p>
<div class="mt-3 space-y-3">
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-xs text-zinc-400">Início</p>
<div
class="mt-1 flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-28 rounded bg-zinc-800"></span>
</div>
</div>
<div>
<p class="text-xs text-zinc-400">Fim</p>
<div
class="mt-1 flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-28 rounded bg-zinc-800"></span>
</div>
</div>
</div>
<div class="divide-y divide-zinc-800 rounded-lg border border-zinc-800 px-4">
<label class="flex items-start gap-3 py-3">
<input type="checkbox" class="mt-0.5 h-4 w-4 accent-zinc-500" />
<span>
<span class="block text-sm text-zinc-300">Todo o dia</span>
<span class="block text-xs text-zinc-500"
>Compromisso que ocupa um ou mais dias inteiros, sem horário.</span
>
</span>
</label>
<label class="flex items-start gap-3 py-3">
<input type="checkbox" class="mt-0.5 h-4 w-4 accent-amber-400" />
<span class="min-w-0 flex-1">
<span class="flex items-center gap-2">
<span class="text-sm font-medium text-zinc-100">Evento público</span>
<span
class="rounded-full border border-amber-400/50 px-1.5 text-[10px] uppercase tracking-wide text-amber-400"
>neste item</span
>
</span>
<span class="mt-0.5 block text-xs text-zinc-400"
>Quando marcado, este evento poderá ser publicado no site do candidato
(futuro).</span
>
</span>
</label>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-3 border-t border-zinc-800 px-6 py-4">
<span
class="inline-flex h-10 items-center rounded-lg border border-zinc-700 px-4 text-sm text-zinc-300"
>Cancelar</span
>
<span
class="inline-flex h-10 items-center rounded-lg bg-amber-400 px-4 text-sm font-medium text-zinc-950"
>Salvar</span
>
</div>
</div>
</section>
<section class="mt-10">
<p class="text-xs uppercase tracking-wide text-zinc-500">
Cena 2 · mobile (~390px) — mesma cena
</p>
<div
class="mt-3 w-[390px] max-w-full overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900"
>
<div class="flex items-center justify-between border-b border-zinc-800 px-4 py-4">
<div>
<p class="text-sm font-medium text-zinc-200">Editar atividade</p>
<p class="text-xs text-zinc-500">/campanha/atividades</p>
</div>
<span class="text-lg text-zinc-500">×</span>
</div>
<div class="px-4 py-4">
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">
Informações básicas
</p>
<div class="mt-3 space-y-3">
<div>
<p class="text-xs text-zinc-400">Título</p>
<div
class="mt-1 flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-44 rounded bg-zinc-800"></span>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-xs text-zinc-400">Tipo</p>
<div
class="mt-1 flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-16 rounded bg-zinc-800"></span>
<span class="text-xs text-zinc-600">▾</span>
</div>
</div>
<div>
<p class="text-xs text-zinc-400">Status</p>
<div
class="mt-1 flex h-11 items-center justify-between rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-16 rounded bg-zinc-800"></span>
<span class="text-xs text-zinc-600">▾</span>
</div>
</div>
</div>
<label class="flex items-start gap-3 pt-1">
<input type="checkbox" class="mt-0.5 h-4 w-4 accent-zinc-500" />
<span>
<span class="block text-sm text-zinc-300">Deputado presente</span>
<span class="block text-xs text-zinc-500"
>Marque quando o deputado estiver presente na atividade.</span
>
</span>
</label>
</div>
<p class="mt-6 text-xs font-medium uppercase tracking-wide text-zinc-500">
Agendamento
</p>
<div class="mt-3 space-y-3">
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-xs text-zinc-400">Início</p>
<div
class="mt-1 flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-20 rounded bg-zinc-800"></span>
</div>
</div>
<div>
<p class="text-xs text-zinc-400">Fim</p>
<div
class="mt-1 flex h-11 items-center rounded-lg border border-zinc-700 bg-zinc-950/60 px-3"
>
<span class="h-3 w-20 rounded bg-zinc-800"></span>
</div>
</div>
</div>
<div class="divide-y divide-zinc-800 rounded-lg border border-zinc-800 px-4">
<label class="flex items-start gap-3 py-3">
<input type="checkbox" class="mt-0.5 h-4 w-4 accent-zinc-500" />
<span>
<span class="block text-sm text-zinc-300">Todo o dia</span>
<span class="block text-xs text-zinc-500"
>Compromisso que ocupa um ou mais dias inteiros, sem horário.</span
>
</span>
</label>
<label class="flex items-start gap-3 py-3">
<input type="checkbox" class="mt-0.5 h-4 w-4 accent-amber-400" />
<span class="min-w-0 flex-1">
<span class="flex flex-wrap items-center gap-2">
<span class="text-sm font-medium text-zinc-100">Evento público</span>
<span
class="rounded-full border border-amber-400/50 px-1.5 text-[10px] uppercase tracking-wide text-amber-400"
>neste item</span
>
</span>
<span class="mt-0.5 block text-xs text-zinc-400"
>Quando marcado, este evento poderá ser publicado no site do candidato
(futuro).</span
>
</span>
</label>
</div>
</div>
</div>
<div class="flex flex-col gap-3 border-t border-zinc-800 px-4 py-4">
<span
class="inline-flex h-11 items-center justify-center rounded-lg border border-zinc-700 text-sm text-zinc-300"
>Cancelar</span
>
<span
class="inline-flex h-11 items-center justify-center rounded-lg bg-amber-400 text-sm font-medium text-zinc-950"
>Salvar</span
>
</div>
</div>
</section>
<p class="mt-10 text-xs text-zinc-600">
Rascunho descartável (gate) — registro do aceite visual. Zero JS além do build browser do
Tailwind v4; sem comportamento.
</p>
</main>
</body>
</html>

View file

@ -0,0 +1,94 @@
# Atividade: marcar evento como público (base para exposição no site)
Status: rascunho
Atualizado em: 2026-08-27
Issue: #936
Priority: P3
Model: composer-2.5
Serializes: #933 (C148)
Impeccable: B — encaixe no modal de atividade
Rascunho UI: docs/plans/c151-atividade-evento-publico-ui-draft.html
Appetite: ~0,5 dia eng; um outcome verificável — o staff marca/desmarca "Evento público" e a marcação é durável e consultável, sem mudar nada no comportamento atual das telas
Responsável: —
## Intenção
"Let's add a new property to activities to register it as a public event. Public events will later be exposed in the candidate's website to be shareable for followers/supporters." — hoje uma atividade (caminhada, comício, agenda do mandato) é só registro interno: quando o time quiser divulgar um evento no site, não há como distinguir o que é público do que é operação interna. Este item adiciona essa distinção — uma marcação simples, durável e consultável — para que a futura agenda pública do site tenha um dado confiável de onde partir. A exposição em si é outro item (PUB): aqui só se marca a intenção.
## Persona e fluxo
- **Persona / contexto:** staff de campanha (coordenador/assessor/candidato) editando atividades em `/campanha/atividades`, no escritório ou em campo; liderança não entra nesta história.
- **Job principal:** ao cadastrar/editar uma atividade, marcar se ela é um evento público — sem planilha paralela nem precisar lembrar disso depois.
- **Fluxo desejado:** abrir o modal da atividade → marcar/desmarcar "Evento público" (desmarcado por padrão) → salvar; a marcação persiste e volta correta ao reabrir. Nenhuma tela muda de comportamento.
- **Anti-goals de produto:** página pública ou share kit agora; segundo cadastro de "evento" paralelo à atividade; filtro/badge nova na lista; flag alterando o espelho do Google ou o access atual.
### Esboço de fluxo (B/C/D)
```text
[início: modal de atividade] → staff marca "Evento público" (default: desmarcado)
→ salvar → [outcome: marcação durável e consultável; nenhuma tela muda de comportamento]
```
### Rascunho UI (B/C/D)
- Rascunho UI (gate): `docs/plans/c151-atividade-evento-publico-ui-draft.html` — cenas desktop + mobile (~390px) do modal com o novo checkbox ao lado dos existentes.
## Objetivo e aceite
- O staff que edita atividades marca/desmarca "Evento público"; a marcação persiste entre sessões, volta correta ao reabrir a atividade e fica consultável para o consumidor futuro — sem nenhum comportamento novo visível nas telas.
- **Guardrails de produto:** a flag marca INTENÇÃO de exposição; a exposição é FUTURA (item PUB posterior). Nada no site público muda; nada na agenda muda de comportamento.
- **Guardrail para o FUTURO consumidor:** atividade pública nunca expõe lideranças, tarefas, resultados nem dados operacionais internos — a superfície pública futura mostra o "cartaz do evento" (título, data/hora, município/local, descrição), nunca o backend operacional.
- Quem marca é o mesmo staff que edita atividades (coordenador/assessor/candidato); liderança segue em lockdown e não vê o campo nem nada novo.
## Dados (intenção)
- **Vou apresentar dados?** Não — nenhuma superfície de dados neste item; a flag é insumo durável para o item PUB futuro (agenda pública, PUB3 #45, blocked).
- **Decisões desbloqueadas:** staff + escolha de quais atividades são candidatas a exposição pública; o item PUB futuro decide o quê/como expor.
- **Forma:** _adiada ao plano de implementação_ — aqui só a restrição de produto acima (cartaz do evento, nunca backend operacional).
## Dados da decisão (literais)
- Rótulo de UI (checkbox): "Evento público" — default desmarcado.
- Descrição admin pt-BR (curta): "Quando marcado, este evento poderá ser publicado no site do candidato (futuro)."
- Quem pode marcar: os mesmos papéis que editam atividades hoje (coordinator/advisor/candidate); liderança não vê o campo.
## Direção no codebase (hipótese)
- **Áreas prováveis:** collection `activity` (`src/collections/Activity.ts`), modal central `src/components/campaign/activity/` (ActivityOverlay — create+edit), detalhe `/campanha/atividades/[slug]`; Payload admin herda.
- **Precedente a olhar:** checkboxes no mesmo arquivo — `deputyPresent` (~L493) e `allDay` (~L508), ambos checkbox com default desmarcado, indexado e descrição admin; migrations recentes de campo simples (`20260819_213947_add_site_settings_facebook_pixel_id`, `20260823_161905_add_municipality_update_deliberation`) + regeneração de tipos.
- **Risco de acoplamento:** `SYNC_RELEVANT_ACTIVITY_FIELDS` (`googleCalendarSync.ts:100`) decide se campo dispara re-sync — recomendação v1: NÃO incluir (calendário oficial segue full-staff). Access atual (`canReadActivity` inclui leader) NÃO muda; flag de negócio nova não pede field-access especial.
## Dependências
- **Serializa com C148** (mesmo modal de atividade — não trabalhar em paralelo).
- PUB3 (#45) — consumidor futuro da flag (agenda pública); está blocked e NÃO é desbloqueado por este item (aqui não se constrói exposição).
- C114 (espelho do Google) — interação a respeitar: decisão v1 = flag não influencia o sync.
## Fora de escopo
- Página/agenda pública no site e share kit — item PUB futuro (PUB3 #45); nada no `(frontend)` muda.
- Qualquer consumo/exposição da flag agora (site, feed, filtro, badge na lista).
- Collection nova ou modelagem paralela de "evento público" — a marcação vive na atividade existente.
- Mudança de access/visibilidade (liderança continua sem ver nada novo; Payload admin segue como está).
- Integração da flag com o espelho do Google (decisão v1: não influencia).
- Marcação em lote de atividades existentes (spreadsheet mode — sem pedido).
## Rabbit holes de produto
- **"Já deixar a página pública pronta."** Se alguém "só completar": agenda pública, SEO, share kit — explosão de escopo. **Corte neste item:** só a marcação; exposição é item PUB futuro.
- **"Aproveitar e ligar a flag ao espelho do Google."** Se alguém "só completar": re-escopo do calendário oficial (full-staff) e conflito com C114. **Corte neste item:** sync intocado em v1.
- **"Criar collection 'evento público' separada."** Se alguém "só completar": segundo cadastro de atividade, dados divergentes. **Corte neste item:** flag na atividade existente.
- **"Filtrar/badgear a lista por eventos públicos."** Se alguém "só completar": redesign de tabs/filtros da agenda. **Corte neste item:** nenhuma tela muda de comportamento.
## Questões em aberto (produto)
- **Em qual seção do modal entra o checkbox?** **Opções:** A) junto dos outros checkboxes do modal (seção básica/agendamento) | B) seção própria "Publicação" | C) campo avançado/oculto. **Recomendação:** A — um checkbox a mais não pede seção nova; o rascunho o mostra ao lado dos existentes. _(assumido — validar no gate com o rascunho)_
- **A flag influencia o espelho do Google?** **Opções:** A) NÃO em v1 — calendário oficial continua full-staff; a flag existe para o site futuro | B) sim, re-scope do sync. **Recomendação:** A — mudar o sync agora complica C114 sem ganho de produto hoje. _(recomendação — validar com produto)_
## Referências
- GitHub Issue: — (após `pnpm agent:register`)
- Rascunho UI (gate): `docs/plans/c151-atividade-evento-publico-ui-draft.html`
- `src/collections/Activity.ts` (precedentes `deputyPresent`/`allDay`) · `src/components/campaign/activity/ActivityOverlay.tsx`
- `src/utilities/access/activities.ts` · `src/utilities/googleCalendarSync.ts:100` · `src/collections/Post.ts:78` (precedente público mais próximo: `Post.type` valor `evento`)
- `AGENTS-campaign.md` — seção "Campaign activities (C3)" · PUB3 (#45)