docs(plans): plano de intenção OPS80/OPS79/OPS81 — cutoff da URL pt.jorgesolla.com.br (desativar → migrar → redirecionar)

This commit is contained in:
Francisco Solla 2026-08-23 14:59:30 -03:00
parent dddbd920e0
commit d157554f0e
3 changed files with 216 additions and 0 deletions

View file

@ -0,0 +1,73 @@
# Última migração de dados em produção: plataforma antiga → nova (vertical campanha)
Status: rascunho
Atualizado em: 2026-08-23
Issue: #797
Priority: P1
Impeccable: A — sem UI
Rascunho UI: N/A — sem UI
Appetite: ~2–3 dias eng; um outcome verificável (nenhum dado de operação de campanha viver só na plataforma antiga antes do desligamento)
Responsável: —
## Intenção
Durante a transição, o site continuou vivo em DUAS plataformas: a antiga `pt.jorgesolla.com.br` (Vercel + banco Neon) e a nova `jorgesolla1313.com.br` (homeserver, banco próprio). O time de campanha seguiu usando `/campanha` na URL antiga; depois do OPS51 (migração Neon→homeserver, 2026-08-17), qualquer dado gravado na plataforma antiga ficou órfão da nova. **OPS80 desativa a URL antiga primeiro, congelando a fonte** (nenhuma escrita nova). Este item roda DEPOIS da desativação: trazer para o banco novo o estado final da plataforma antiga, PRINCIPALMENTE a vertical campanha (lideranças, pledges, apoiadores, atividades, usuários da campanha, etc.) — para nenhuma operação de campanha viver só na plataforma antiga no momento em que ela for desligada.
## Persona e fluxo
- **Persona / contexto:** coordinator e staff de campanha que operam `/campanha` diariamente em janela eleitoral ativa; engenharia/ops responsável pelo desligamento da plataforma antiga.
- **Job principal:** nenhum dado de campanha — nem um pledge, apoiador, liderança, atividade ou usuário — viver só na plataforma antiga no momento do desligamento.
- **Fluxo desejado:** OPS80 desativa a URL antiga (fonte congelada, sem escritas novas) → este item captura o estado final da plataforma antiga → o residual é reconciliado e trazido para a nova → contagens conferem (nada órfão, nada duplicado) → `/campanha` segue operando só na plataforma nova, sem perda e sem regressão → a URL antiga permanece desligada.
- **Anti-goals de produto:** NÃO re-migrar tudo por desconfiança; NÃO duplicar municípios/seed nem dados públicos já sincronizados; NÃO virar "conserto" de divergências históricas; NÃO criar superfície nova de edição de dados (isso é planilha-modo).
## Objetivo e aceite
- Após o item, todos os dados de operação de campanha existentes na plataforma antiga existem e estão íntegros na nova — contagens conferem e nenhum registro ficou para trás.
- Nenhuma operação de campanha registrada na plataforma antiga (pledges, apoiadores, lideranças, atividades, demandas, decisões de alocação) precisa ser refeita manualmente na nova.
- Usuários da campanha continuam acessando `/campanha` na plataforma nova sem perda de acesso (senhas/sessões preservadas ou re-emitidas sem re-cadastro).
- Guardrails: dados nominais de cidadãos (PII/LGPD) só migram com o mesmo fail-closed do app (consentimento por chave estável); municípios/semeados não são duplicados; a vertical não regride (assimetria de votos, lockdown de liderança, leitura relativa/local preservadas).
## Dados (intenção)
- **Vou apresentar dados?** Não — migração move dados, não apresenta superfície de decisão.
- **Decisões desbloqueadas:** nada novo que dependa de um dashboard; o que importa é que decisões JÁ existentes continuem íntegras pós-migração: (a) conta da cadeira, (b) mapas e leitura territorial, (c) pledges com `declaredVotes` vs `estimatedVotes` e seus agregados devem refletir o MESMO estado que o time via na plataforma antiga.
- **Forma:** *adiada ao plano de implementação* — aqui só a restrição de produto: o pós-migração precisa produzir a mesma leitura relativa/local que a plataforma antiga mostrava (sem "conserto" silencioso de números).
## Direção no codebase (hipótese)
- **Áreas prováveis:** coleções da vertical campanha (`src/collections/`) e a área de campanha (`src/app/(campaign)/…`, `src/utilities/access/*`); precedente de tooling de dados em `scripts/` (ex.: `scripts/db-pull.mjs`, `pnpm db:pull`).
- **Precedente a olhar:** OPS51 (dump Neon→homeserver, contagens por coleção), OPS58 (sincronização de posts públicos), OPS52 (media) — docs de ops em `docs/ops/` e `docs/plans/`.
- **Risco de acoplamento:** o executor deve respeitar as guardas de dev/test (nunca apontar dev/test para prod) e o fail-closed de consentimento/LGPD — esta migração é operação manual de infra em prod, fora dos guards de dev.
## Dependências
- **OPS80 (duro):** a desativação/congelamento da URL antiga vem ANTES — a fonte precisa estar quiescida para que nenhum dado novo entre entre a última migração e o desligamento.
- Nenhuma dependência suave.
## Fora de escopo
- Cutover de DNS/hospedagem de `pt.jorgesolla.com.br` (OPS80 desativa + OPS81 redireciona).
- Migração de media — já feita em OPS52 (Garage S3); referências antigas são exceção a registrar, não re-migração.
- Cancelamento da conta Neon (pós-OPS80).
- Dados públicos já sincronizados (ex.: posts — OPS58).
## Rabbit holes de produto
- **Merge de delta vs reconciliação limpa.** Se alguém "só completar" com o delta pós-OPS51, uma divergência silenciosa anterior continua escondida; se reconciliar tudo, vira re-migração completa. **Corte neste item:** reconciliar a vertical inteira uma última vez (contagens vs dump OPS51 como linha de base), com o delta como piso — sem re-migrar o que já está íntegro.
- **Copiar dados públicos duplicados.** Trazer posts/assinaturas/signatures de novo criaria duplicidade com o que já foi sincronizado. **Corte:** apenas a vertical campanha é o foco; o resto só se aparecer residual real e verificável.
- **"Consertar" divergências históricas de IDs.** Em vez de aceitar que a fonte de verdade é a nova, alguém pode tentar reconciliar históricos de IDs que já divergiram. **Corte:** nenhuma correção de IDs pós-fato; o aceite é integridade de dados de campanha, não identidade perfeita entre plataformas.
- **Scope creep para re-migrar tudo.** A desconfiança pós-OPS51 pode inflar o item para "re-fazer a migração inteira". **Corte:** escopo é o residual + reconciliação da vertical campanha; fora disso, OPS80 decide.
## Questões em aberto (produto)
- **Qual é a base real da plataforma antiga hoje (Vercel ainda aponta para o Neon)?** **Opções:** (a) Neon — assumir que toda escrita pós-OPS51 foi no Neon e migrar esse residual; (b) outra base — a plataforma antiga já foi repontada e o gap é outro; (c) plataforma antiga inativa desde o OPS51. **Recomendação:** verificação viva (read-only, escrita de teste e leitura de volta em ambas as plataformas) ANTES do cutoff de OPS80 — sem essa verificação, a migração pode estar varrendo o banco errado. A cópia em si roda depois, sobre a fonte congelada. _(assumido — validar com engenharia no momento do desligamento)_
- **Migrar só o delta pós-OPS51 ou reconciliar a vertical inteira?** **Opções:** (a) só delta — rápido, mas deixa divergência silenciosa possível; (b) reconciliar a vertical inteira — mais demorado, mas fecha o risco de dado órfão anterior ao OPS51; (c) delta + reconciliação por contagem. **Recomendação:** (c) — reconciliar por contagem (a mesma verificação do OPS51) com o delta como objeto da cópia. É o único caminho que dá o aceite "nada órfão, nada duplicado" sem re-migrar tudo.
- **O que fazer com senhas/sessões de usuários da campanha?** **Opções:** (a) preservar hashes e sessões como estão — nenhuma interrupção no meio da janela eleitoral; (b) exigir re-login após a migração; (c) re-emitir convites. **Recomendação:** (a) — campanha em janela eleitoral ativa; forçar re-login em massa seria a pior hora para interromper operação. Se a base de origem não for confiável para credenciais, cair em (b), nunca em (c).
- **Referências a media antiga em registros migrados?** **Opções:** (a) migrar apontando para o objeto já no Garage S3; (b) manter URL antiga e registrar exceção; (c) quebrar a referência. **Recomendação:** (a) quando houver equivalente já migrado (OPS52), (b) como exceção registrada caso contrário — nunca (c), imagem quebrada é regressão visível.
## Referências
- GitHub Issue #— (após `pnpm agent:register`)
- OPS51 (migração Neon→homeserver, contagens por coleção) e seu rollback documentado em `docs/ops/teqo-1313-deploy.md`
- OPS52 (media → Garage S3), OPS58 (sincronização de posts públicos), OPS80 (desativa a URL antiga — pré-requisito que congela a fonte), OPS81 (redireciona a URL antiga — roda após esta migração)
- `AGENTS.md` / `AGENTS-campaign.md` — vertical campanha, LGPD (fail-closed de consentimento), guardas de dev/test

View file

@ -0,0 +1,71 @@
# Cutoff de transição (fase 1) — desativar o acesso ao site pela URL pt.jorgesolla.com.br (congelar a fonte)
Status: rascunho
Atualizado em: 2026-08-23
Issue: #796
Priority: P1
Impeccable: A — sem UI
Rascunho UI: N/A — sem UI
Appetite: ~1 dia eng; um outcome verificável (a URL antiga deixa de servir o app — fonte congelada, sem escritas novas)
Responsável: —
## Intenção
A transição acabou: o site novo vive em `jorgesolla1313.com.br` (homeserver + túnel Cloudflare), mas `pt.jorgesolla.com.br` continua no ar servindo o app pela plataforma antiga (Vercel) — DUAS fontes de verdade, tráfego dividido, e qualquer dado gravado na URL antiga fica órfão da nova. **Fase 1 do cutoff (este item):** o site deixa de ser servido pela URL antiga — o que **congela a fonte** (nenhuma escrita nova na plataforma antiga). O redirecionamento NÃO entra aqui: ele acontece só DEPOIS da última migração de dados (OPS79), na fase 2 (OPS81) — decisão do humano, para ninguém cair no domínio canônico antes de ele ter os dados finais.
## Persona e fluxo
- **Persona / contexto:** (1) time de campanha — assessores e lideranças que ainda usam `pt.jorgesolla.com.br/campanha`; (2) ops/eng — hoje obrigada a manter e vigiar duas plataformas.
- **Job principal:** congelar a plataforma antiga — parar de servir o app pela URL antiga e impedir qualquer escrita nova — abrindo caminho para a última migração (OPS79) sobre uma fonte estável.
- **Fluxo desejado:** o cutoff desliga o app na URL antiga (fonte congelada) → OPS79 captura o estado final e traz para a nova → (fase 2, OPS81) a URL antiga passa a redirecionar para a canônica. Neste item, a URL antiga fica num estado de transição (fora do ar/sem interação), não servindo o app nem ainda redirecionando.
- **Anti-goals de produto:** não deixar a URL antiga servindo o app em qualquer pedaço; não redirecionar já (redirect é OPS81, pós-migração); não interromper o acesso do time a `/campanha` **no domínio canônico** (que segue no ar normalmente).
## Objetivo e aceite
- `pt.jorgesolla.com.br` deixa de servir o app — um acesso pela URL antiga **não** inicia mais sessão, formulário ou qualquer escrita (fonte congelada).
- Nenhuma escrita nova é possível via URL antiga a partir do cutoff — o gap "dados novos entre a última migração e a desativação" fica estruturalmente impossível.
- A URL antiga **não** redireciona ainda para o canônico (isso é OPS81, após OPS79) — estado de transição aceito, janela curta.
- A vertical de campanha no domínio canônico (`jorgesolla1313.com.br/campanha`) continua funcionando sem regressão.
## Dados (intenção)
- **Vou apresentar dados?** Não.
- **Decisões desbloqueadas:** o cutoff é uma decisão de política (uma única fonte de verdade), não de dado.
- **Forma:** *adiada ao plano de implementação* — sem restrição de produto além do acima.
## Direção no codebase (hipótese)
- **Áreas prováveis:** superfície que referencia `pt.jorgesolla.com.br` no repo — scripts de verificação (`scripts/check-push-chain.mjs`), testes unit/int (WebAuthn config, json mutation route, multi-phones, aiMarkdownLinks), docs históricos (ops50/51, c98-impl, revalidate-secret, push-notificacoes, c115, CUTOVER-MAIN-ONLY) e personas de agentes (`.opencode/agent/`). A desativação em si é infra de hosting (Vercel/DNS), fora do app.
- **Precedente a olhar:** histórico de OPS50 (remoção de artefatos Vercel), OPS51/OPS52 (cutover adiado, "Excluir a store do Vercel Blob — só após cutover estável").
- **Risco de acoplamento:** a URL canônica (`NEXT_PUBLIC_SITE_URL`) já é obrigatória em prod e governa invite, calendarFeed, syncs, passwordReset e WebAuthn — o executor deve manter o app 100% canônico e não criar caminhos dependentes da URL antiga.
## Dependências
- **Nenhuma** — este item é o PRIMEIRO da sequência do cutoff (desativação → migração → redirect).
- OPS79 (migração) **depende** deste: a fonte precisa estar congelada antes da última migração.
## Fora de escopo
- Última migração de dados (OPS79) — roda depois, sobre a fonte congelada.
- Redirecionamento da URL antiga (OPS81) — só após a migração.
- Cancelamento financeiro das contas Neon/Vercel — decisão de infra pós-cutoff, fora deste item.
- Remoção de `@vercel/analytics`/`@vercel/speed-insights` — não relacionada ao domínio.
- SEO/canonical tags do site público — não é este item.
## Rabbit holes de produto
- **Manter a plataforma antiga viva "por precaução".** Se alguém "só completar" mantendo a Vercel no ar, seguem duas fontes de verdade e o congelamento não acontece. **Corte neste item:** a URL antiga deixa de servir o app; ponto.
- **Já redirecionar na fase 1.** Apressar o redirect deixaria o canônico (ainda sem a migração) como destino de quem vem da URL antiga — exatamente o gap de dados intermediários que o humano quer evitar. **Corte:** o redirect é OPS81, após OPS79.
- **SEO/canonical vira redesenho.** Melhorar ranking/`<link rel=canonical>` do site público é outro esforço. **Corte neste item:** foco na transição de domínio; SEO é plano próprio.
## Questões em aberto (produto)
- **O que a URL antiga deve responder no intervalo até o redirect (OPS81)?** **Opções:** (A) estado neutro estático (hold: "site em migração", sem formulários/escritas); (B) fora do ar (não responde / erro). **Recomendação:** (A) — informa quem caiu de paraquedas sem servir o app nem aceitar escrita; janela curta, em seguida OPS79 migra e OPS81 redireciona. _(assumido — validar com produto)_
- **Como efetivar a desativação?** **Opções:** (A) remover o domínio/alias da plataforma antiga (Vercel) e derrubar o deployment; (B) repontar DNS para estado morto. **Recomendação:** decisão do executor; o requisito é o app não servir a URL antiga e nenhuma escrita ser possível.
## Referências
- Estado vivo (2026-08-23): `pt.jorgesolla.com.br` responde 200 via Vercel; `jorgesolla1313.com.br` responde 200 via Cloudflare (túnel do homeserver); sem host-redirect no app.
- Superfície que referencia a URL antiga no repo: `scripts/check-push-chain.mjs:13,47,49`; `tests/unit/campaignWebAuthnConfig.unit.spec.ts:17-52`; `tests/unit/campaignJsonMutationRoute.unit.spec.ts:27,54`; `tests/int/campaignMultiPhones.int.spec.ts:336`; `tests/unit/aiMarkdownLinks.unit.spec.ts:66`; `.opencode/agent/designer-campanha-solla.md:9,21`; `.opencode/agent/solla-comunicacao.md:19`.
- Docs históricos do cutover: `docs/plans/ops50*`, `ops51*`, `ops52-...-impl.md` (linha 104 — Vercel Blob pós-cutover), `CUTOVER-MAIN-ONLY.md`.
- `AGENTS.md` (Teqo context) — OPS50 removeu artefatos Vercel; `NEXT_PUBLIC_SITE_URL` obrigatória em prod via `getCampaignInviteBaseURL`.

View file

@ -0,0 +1,72 @@
# Cutoff de transição (fase 2) — redirecionar pt.jorgesolla.com.br/campanha para jorgesolla1313.com.br/campanha
Status: rascunho
Atualizado em: 2026-08-23
Issue: #798
Priority: P1
Impeccable: A — sem UI
Rascunho UI: N/A — sem UI
Appetite: ~0,5–1 dia eng; um outcome verificável (a URL antiga termina na canônica, com path preservado)
Responsável: —
## Intenção
Depois da desativação (OPS80, que congelou a fonte) e da última migração de dados (OPS79), falta a **fase 2 do cutoff**: dar vida útil à URL antiga como mero redirecionador. Quem ainda acessar `pt.jorgesolla.com.br/campanha` — bookmarks, links de WhatsApp/QR, material impresso — cai direto na vertical de campanha do domínio canônico `jorgesolla1313.com.br/campanha`. O redirecionamento vem SÓ depois da migração, por decisão do humano: assim ninguém cai no canônico antes de ele ter os dados finais (nenhum "dado intermediário" fica de fora da leitura).
## Persona e fluxo
- **Persona / contexto:** (1) time de campanha — assessores e lideranças com bookmarks/links antigos, em campo, acessando `/campanha` no celular; (2) visitante público com link antigo (WhatsApp/QR/email antigo); (3) ops/eng — fecha o cutoff com uma única fonte de verdade.
- **Job principal:** quem ainda usar a URL antiga chega ao destino certo na canônica — especialmente `/campanha` — sem página morta e sem dados faltando.
- **Fluxo desejado:** usuário abre/digita a URL antiga → é redirecionado para a URL canônica preservando o path (`/campanha` → `/campanha`) → segue navegando em `jorgesolla1313.com.br` com os dados finais já migrados.
- **Anti-goals de produto:** não deixar a URL antiga servindo o app em qualquer pedaço; não redirecionar antes da migração (a ordem desativação→migração→redirect é decisão do humano); não transformar o cutoff num redesenho de SEO/canonical.
## Objetivo e aceite
- `pt.jorgesolla.com.br/campanha` redireciona para `jorgesolla1313.com.br/campanha` preservando o destino — o time segue operando com links antigos, sem perder sessão de trabalho.
- Idealmente toda rota da URL antiga redireciona para a canônica com path preservado (o resto do site também deixa de ter URL morta) — `/campanha` é o caso obrigatório.
- A URL antiga continua **não** servindo o app (nenhuma escrita possível — fonte segue congelada).
- A vertical de campanha no domínio canônico continua funcionando — WebAuthn e sessão seguem válidas no host que serve a página (rpID/origin casam com o domínio canônico), sem regressão.
- Ops/eng deixa de manter a URL antiga como plataforma viva (deixa de existir decisão de "qual URL é a canônica").
## Dados (intenção)
- **Vou apresentar dados?** Não.
- **Decisões desbloqueadas:** nenhuma decisão de produto depende de métrica aqui; o cutoff é uma decisão de política (uma única fonte de verdade), não de dado.
- **Forma:** *adiada ao plano de implementação* — sem restrição de produto além do acima.
## Direção no codebase (hipótese)
- **Áreas prováveis:** infra de hosting da URL antiga (o redirecionador roda fora do app — Cloudflare/DNS/host estático; o repo não tem host-redirect hoje, sem `src/middleware.ts`/`proxy.ts`; todos os `redirect()` são por path relativo). Superfície de repo a rever: `scripts/check-push-chain.mjs`, testes unit/int que pinam a URL antiga e docs/personas de agentes (`.opencode/agent/`).
- **Precedente a olhar:** histórico de OPS50 (remoção de artefatos Vercel), OPS51/OPS52 (cutover adiado).
- **Risco de acoplamento:** a URL canônica (`NEXT_PUBLIC_SITE_URL`) já é obrigatória em prod e governa invite, calendarFeed, syncs, passwordReset e WebAuthn — o executor deve manter o app 100% canônico e não criar caminhos dependentes da URL antiga.
## Dependências
- **OPS79 (dura)** — o redirecionamento só acontece DEPOIS da última migração: quem cair no canônico precisa já ver os dados finais. Sem isso, o redirect criaria o exato gap de dados intermediários que a ordem desativação→migração→redirect evita.
- OPS80 (desativação) é pré-requisito estrutural da sequência (já concluído quando este item roda).
## Fora de escopo
- Desativação da URL antiga (OPS80) — já feita, fase 1.
- Última migração de dados (OPS79) — já feita, entre as duas fases.
- Cancelamento financeiro das contas Neon/Vercel — decisão de infra pós-cutoff, fora deste item.
- Remoção de `@vercel/analytics`/`@vercel/speed-insights` — não relacionada ao domínio.
- SEO/canonical tags do site público — não é este item.
## Rabbit holes de produto
- **Redirecionar só `/campanha`.** Deixaria o resto da URL antiga morto sem destino — usuário com link antigo de artigo/capa cairia em erro. **Corte neste item:** path preservado para todas as rotas; `/campanha` é o caso obrigatório, o resto é grátis.
- **Redirecionar antes da migração.** Apressar o redirect devolveria o canônico sem os dados finais para quem vem da URL antiga. **Corte:** ordem travada — desativação (OPS80) → migração (OPS79) → redirect (este item).
- **SEO/canonical vira redesenho.** Melhorar ranking/`<link rel=canonical>` do site público é outro esforço. **Corte neste item:** foco na transição de domínio; SEO é plano próprio.
## Questões em aberto (produto)
- **Redirecionar TODAS as rotas da URL antiga ou só `/campanha`?** **Opções:** (A) todas as rotas, preservando path; (B) só `/campanha`. **Recomendação:** (A) — `/campanha` é caso obrigatório; o resto é grátis e elimina qualquer URL morta. _(assumido — validar com produto)_
- **Onde terminar o redirecionamento?** **Opções:** (A) no nível de hosting/DNS (fora do app); (B) dentro do app. **Recomendação:** (A) — o app continua 100% canônico e o cutoff não depende de deploy; decisão de onde exatamente é do executor. _(assumido — validar com produto)_
## Referências
- Estado vivo (2026-08-23): `pt.jorgesolla.com.br` responde 200 via Vercel (antes da fase 1); `jorgesolla1313.com.br` responde 200 via Cloudflare (túnel do homeserver); sem host-redirect no app.
- Superfície que referencia a URL antiga no repo: `scripts/check-push-chain.mjs:13,47,49`; `tests/unit/campaignWebAuthnConfig.unit.spec.ts:17-52`; `tests/unit/campaignJsonMutationRoute.unit.spec.ts:27,54`; `tests/int/campaignMultiPhones.int.spec.ts:336`; `tests/unit/aiMarkdownLinks.unit.spec.ts:66`; `.opencode/agent/designer-campanha-solla.md:9,21`; `.opencode/agent/solla-comunicacao.md:19`.
- Docs históricos do cutover: `docs/plans/ops50*`, `ops51*`, `ops52-...-impl.md` (linha 104 — Vercel Blob pós-cutover), `CUTOVER-MAIN-ONLY.md`.
- `AGENTS.md` (Teqo context) — OPS50 removeu artefatos Vercel; `NEXT_PUBLIC_SITE_URL` obrigatória em prod via `getCampaignInviteBaseURL`.