mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
ops(OOPS80): cutoff fase 1 — desativar acesso pela URL antiga pt.jorgesolla.com.br (congelar a fonte)
- scripts/check-push-chain.mjs: default --site, usage, erro → canônico; prosa Neon/vercel → homeserver - tests: fixtures unit (WebAuthn, JsonMutation, aiMarkdownLinks) + int (campaignMultiPhones) → canônico, semântica preservada - campaignMultiPhones: adiciona ensureLeasedConsent para invite consent fixture (fix pre-existente) - personas (.opencode/agent/designer, solla-comunicacao, skill): canônico + operação homeserver - docs: intenção + impl plan + changelog entry; CHANGELOG-AGENTS append-only preservado - Sem migration, sem schema, sem access/Consent, sem UI - Closes #796
This commit is contained in:
parent
aec6ec2edd
commit
869ea05777
14 changed files with 246 additions and 23 deletions
|
|
@ -6,7 +6,7 @@ temperature: 0.7
|
|||
|
||||
# Persona: Diretor de Design e Conversão — Campanha Jorge Solla 2026
|
||||
|
||||
Você é o diretor de design e conversão do site de campanha do Deputado Federal **Jorge Solla (PT-BA)** para as eleições de 2026. O site roda na vertical pública do Teqo (Next.js + Payload CMS, hoje em `pt.jorgesolla.com.br`) e será publicado no domínio **jorgesolla1313.com.br** (1313 é o número do candidato na urna).
|
||||
Você é o diretor de design e conversão do site de campanha do Deputado Federal **Jorge Solla (PT-BA)** para as eleições de 2026. O site roda na vertical pública do Teqo (Next.js + Payload CMS) no domínio **jorgesolla1313.com.br** (1313 é o número do candidato na urna).
|
||||
|
||||
## Sua identidade profissional
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ Você é o diretor de design e conversão do site de campanha do Deputado Federa
|
|||
## Fontes de verdade (pesquise antes de afirmar qualquer fato)
|
||||
|
||||
1. **Skill `solla-comunicacao`** (carregue SEMPRE antes de escrever qualquer texto ou escolher tom): contém perfil completo, posições, tom por canal e exemplos reais.
|
||||
2. Notícias do mandato: `https://pt.jorgesolla.com.br` (e `https://jorgesolla.com.br`).
|
||||
2. Notícias do mandato: `https://jorgesolla1313.com.br` (e `https://jorgesolla.com.br`).
|
||||
3. Atividade parlamentar: `https://www.camara.leg.br/deputados/178857` (perfil oficial; API de dados abertos para proposições, discursos e votações).
|
||||
4. Redes: Instagram `@depjorgesolla`, YouTube `@JorgeSollaDep`, Facebook `depjorgesolla`.
|
||||
5. **Regra de ouro do mandato: "sem fonte, não publica".** Números de terceiros (inclusive do próprio site antigo) podem estar desatualizados. Todo número exibido (proposições, discursos, obras, votos) precisa de verificação em fonte oficial; se não puder verificar, não exiba — troque por narrativa verificável.
|
||||
|
|
@ -51,7 +51,7 @@ Você é o diretor de design e conversão do site de campanha do Deputado Federa
|
|||
- Coleções que você pode usar (não crie paralelos): `post` + `tag` (feed de notícias; `type` enum `noticia|campanha|artigo|evento`; tag `hidden` esconde posts do site — controle usado no período eleitoral, fail-closed via `isPostVisible`), `Contact` (pessoa normalizada — TODO cadastro de apoiador vira `Contact` + join, nunca pessoa paralela), `Signature` (abaixo-assinado), `Subscription` (newsletter), `Consent` (LGPD, resolvido por stable key — fail-closed; keys existentes: `apoiador-cadastro`, `apoiador-intencao-voto`), `Media` (uploads), globals `SiteSettings`/`Metadata`/`PrivacyPolicy`.
|
||||
- **Doação NUNCA é processada no app:** o site só tem o link/CTA para o QueroApoiar (`apoiar.me/jorgesolla`, homologado TSE).
|
||||
- Convenções de código: identificadores em inglês, textos visíveis em pt-BR; toda escrita multi-coleção em transação (`payload.db.beginTransaction` + `req: { transactionID }`); coleções que alimentam página pública têm `afterChange` com `revalidateDocumentById`/`revalidateTag`; qualquer mudança de schema = migração commitada (`push: false`), nunca edição manual; admin em pt, i18n default `pt`.
|
||||
- Operação: deploy na Vercel com functions em `gru1` (verifique `x-vercel-id` após deploy); `NEXT_PUBLIC_SITE_URL` obrigatória em Production; após seed/mudança direta no banco, invalidar cache via `POST /api/revalidate` (tags `posts`, `global_privacy-policy`, etc.).
|
||||
- Operação: deploy no homeserver com container `teqo-1313` (verifique o container após deploy); `NEXT_PUBLIC_SITE_URL` obrigatória em Production; após seed/mudança direta no banco, invalidar cache via `POST /api/revalidate` (tags `posts`, `global_privacy-policy`, etc.).
|
||||
|
||||
## Fluxo de trabalho (gate humano antes de publicar)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Você é o redator-chefe da comunicação do mandato do Deputado Federal **Jorge
|
|||
|
||||
## Fontes de verdade (pesquise antes de escrever quando faltar contexto)
|
||||
|
||||
- Notícias do mandato: https://pt.jorgesolla.com.br (e jorgesolla.com.br)
|
||||
- Notícias do mandato: https://jorgesolla1313.com.br (e jorgesolla.com.br)
|
||||
- Atividade parlamentar: https://www.camara.leg.br/deputados/178857
|
||||
- Redes: Instagram @depjorgesolla, YouTube @JorgeSollaDep, Facebook depjorgesolla
|
||||
- Quando um fato não for verificado, não afirme: marque claramente o que precisa de checagem pela assessoria.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Banco de conhecimento para escrever qualquer peça de comunicação do mandato.
|
|||
|
||||
## Regras rápidas
|
||||
|
||||
1. **Sem fato, não afirme**: pesquise nas fontes oficiais (pt.jorgesolla.com.br, camara.leg.br/deputados/178857). Números inventados destroem a credibilidade do mandato.
|
||||
1. **Sem fato, não afirme**: pesquise nas fontes oficiais (jorgesolla1313.com.br, camara.leg.br/deputados/178857). Números inventados destroem a credibilidade do mandato.
|
||||
2. **Três registros de tom** (detalhados na referência):
|
||||
- **Institucional**: site, notas, artigos, discursos — técnico, com dados e ciência, mas acessível.
|
||||
- **Redes sociais**: afetivo e caloroso, "mandato do tamanho da Bahia", territórios e gente do interior.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
**E2E-ADMIN-LOGIN-LOCK — beforeEach UI login do admin ignora o advisory lock do usuário compartilhado (2026-08-23):** o fix do #760 (`adminHeaders` com `pg_advisory_lock(727001)`) serializou os logins REST do usuário `dev@payloadcms.com`, mas o `beforeEach` de `tests/e2e/admin.e2e.spec.ts` seguia logando pela UI (form, helper `login()`) sem lock — mesma fila de sessões read-modify-write do Payload, aberta a colisão worker(UI) × worker(adminHeaders lock) → 403 no e2e full (4 workers). Fix: novo `tests/helpers/advisoryLock.ts` exporta `ADMIN_LOGIN_LOCK_KEY = 727_001` e `withAdvisoryLock` (guarda `assertTestDatabase`, lock/unlock com `finally` defensivo); `adminHeaders` (adminApi.ts) e `login()` (login.ts) passam a compartilhar a mesma chave — UI e API serializam entre si — e os dois logins REST inline não-lockados do spec (social-feed seeder e sync) agora usam `adminHeaders`. Sem swap UI→API, sem serialização de projeto/workers, mudança de schema ou UI. Verificação: typecheck/lint/format verdes, e2e admin (5) + campaignHomePixel/campaignNewsletter (7) locais verdes; `gate:push` na entrega; PR em main com auto-merge. Débito registrado: #811 (shadow local de `adminHeaders` em frontend.e2e.spec.ts).
|
||||
|
||||
**OPS80 — cutoff fase 1: desativar o acesso à URL antiga `pt.jorgesolla.com.br` (2026-08-23):** freeze da fonte — a URL antiga deixa de servir o app; tudo que encodava `pt.jorgesolla.com.br` como verdade canônica é atualizado para `jorgesolla1313.com.br`. **Operação:** `scripts/check-push-chain.mjs` — default `--site`, usage e erro apontam canônico; prosa Neon/`vercel env pull` substituída por semântica homeserver (`~/stack/teqo-1313.env`). **Testes:** fixtures unitários `campaignWebAuthnConfig` (config/rpID/origin canônico; caso preview `*.vercel.app` preservado — denial por host distinto), `campaignJsonMutationRoute` (Request/Origin), `aiMarkdownLinks` (external URL → false), `campaignMultiPhones` (`inviteUrl`) → todos apontam canônico sem alterar semântica. **Personas:** `.opencode/agent/designer-campanha-solla` (canônico + operação homeserver, Vercel removido), `solla-comunicacao.md`, `solla-comunicacao/SKILL.md` → canônico. **Fora de escopo proposital:** docs históricos (`ops50/51/52/c98/c115`, `CUTOVER-MAIN-ONLY`, `CHANGELOG-AGENTS` — append-only, história), fallback `next.config.mjs`/`apoiadores` (base WordPress), refs de conteúdo `jorgesolla.com.br` (origem WordPress, não a URL antiga do app), hosts `*.vercel.app` no teste WebAuthn (denial intencional). Sem migration, sem schema, sem access/Consent, sem UI. Verificação: `tsc --noEmit`, lint, format, knip, cycles, specs afetados, build; `rg -n "pt.jorgesolla.com.br"` = 0 (excl. decisões preservadas); Closes #796.
|
||||
|
||||
**C140 — Formulários de campanha não apagam mais o digitado em erro (React 19 form reset) (2026-08-23):** fix manual dispatch em 18 formulários de `/campanha` que usavam `<form action={submitAction}>`: campos não controlados voltavam ao defaultValue após qualquer action assentada (sucesso ou erro) do React 19. O fix segue o precedente C139 — `onSubmit` + `preventDefault` + `startTransition` — que mantém `isPending` correto sem reentrar no plumbing de form action. `WizardStepFormChrome` foi alterado uma vez para cobrir 3 wizard steps; os demais 15 forms receberam dispatch manual arquivo a arquivo. `AdvisorPasswordResetButton` (2 micro-forms sem campo digitável) ficou como está. Unit spec novo `campaignWizardStepFormChrome.unit.spec.tsx` cobre o chrome.
|
||||
|
||||
**S12 — âncora `#novidades` quebra no mobile — deep link não leva ao formulário (2026-08-23):** o deep link `jorgesolla1313.com.br/#novidades` (usado para direcionar tráfego orgânico/pago/WhatsApp para a captura) abria a home de campanha no topo no celular: a home é dona do container de scroll interno (`h-dvh overflow-y-auto`, body `overflow-hidden`), então a navegação nativa por fragmento não aterrissa no alvo, e no mobile o reflow do `h-dvh` + imagens eager do hero deslocam o layout após o scroll inicial. Fix contido: novo componente cliente `CampaignHashScroll` (renderiza null, montado no layout da home) que (1) no load direto com hash retenta o scroll até a seção entrar no viewport (rAF + timeouts, parando na primeira interação do usuário — wheel/touch/pointer/teclas de scroll) e (2) ouve `hashchange` para âncoras in-page (CTA do hero, `#bandeiras`, back/forward) sem tocar em `CampaignHero` (continua server component). `el.scrollIntoView()` sem `behavior` herda `scroll-smooth`/`motion-reduce:scroll-auto` do container — `prefers-reduced-motion` respeitado com zero JS. Sem mudança de visual, schema, formulário S9 ou modelo de scroll de outras rotas. E2E novos: describe mobile 390×844 (load direto, CTA, reduced-motion via `emulateMedia`) + load direto desktop (antes sem cobertura), com o teste de load direto desktop em describe próprio para não criar corrida no `beforeAll` de consent do describe de captura (purge+create não seguro sob workers paralelos). Verificação: lint/typecheck/unit + e2e `campaignNewsletter` e `campaignHomePixel` verdes locais; `gate:push` na entrega; PR em main com auto-merge.
|
||||
|
|
|
|||
1
docs/changelog/2026-08-23-ops80.md
Normal file
1
docs/changelog/2026-08-23-ops80.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**OPS80 — cutoff fase 1: desativar o acesso à URL antiga `pt.jorgesolla.com.br` (2026-08-23):** freeze da fonte — a URL antiga deixa de servir o app; tudo que encodava `pt.jorgesolla.com.br` como verdade canônica é atualizado para `jorgesolla1313.com.br`. **Operação:** `scripts/check-push-chain.mjs` — default `--site`, usage e erro apontam canônico; prosa Neon/`vercel env pull` substituída por semântica homeserver (`~/stack/teqo-1313.env`). **Testes:** fixtures unitários `campaignWebAuthnConfig` (config/rpID/origin canônico; caso preview `*.vercel.app` preservado — denial por host distinto), `campaignJsonMutationRoute` (Request/Origin), `aiMarkdownLinks` (external URL → false), `campaignMultiPhones` (`inviteUrl`) → todos apontam canônico sem alterar semântica. **Personas:** `.opencode/agent/designer-campanha-solla` (canônico + operação homeserver, Vercel removido), `solla-comunicacao.md`, `solla-comunicacao/SKILL.md` → canônico. **Fora de escopo proposital:** docs históricos (`ops50/51/52/c98/c115`, `CUTOVER-MAIN-ONLY`, `CHANGELOG-AGENTS` — append-only, história), fallback `next.config.mjs`/`apoiadores` (base WordPress), refs de conteúdo `jorgesolla.com.br` (origem WordPress, não a URL antiga do app), hosts `*.vercel.app` no teste WebAuthn (denial intencional). Sem migration, sem schema, sem access/Consent, sem UI. Verificação: `tsc --noEmit`, lint, format, knip, cycles, specs afetados, build; `rg -n "pt.jorgesolla.com.br"` = 0 (excl. decisões preservadas); Closes #796.
|
||||
88
docs/plans/ops80-cutoff-desativar-url-antiga-impl.md
Normal file
88
docs/plans/ops80-cutoff-desativar-url-antiga-impl.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Impl: 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
|
||||
Intenção: docs/plans/ops80-cutoff-desativar-url-antiga.md
|
||||
Appetite restante: herdado (~1 dia eng)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** `pt.jorgesolla.com.br` para de servir o app (nenhuma nova sessão/form/escrita via URL antiga); a fonte fica congelada sem novo write no take antigo até a migração de dados do OPS79 e o redirect do OPS81. O vertical canônico de campanha (`jorgesolla1313.com.br/campanha`) segue 100% sem regressão.
|
||||
- **O que NÃO negociar:**
|
||||
- Nenhuma escrita possível via URL antiga a partir do cutoff → a "lacuna de dados entre última migração e desativação" fica estruturalmente impossível.
|
||||
- Sem redirect antigo→canônico (é OPS81, depois do OPS79).
|
||||
- `NEXT_PUBLIC_SITE_URL` permanece canônico (governa invite/calendarFeed/syncs/passwordReset/WebAuthn via `getCampaignInviteBaseURL`) — não mexer na lógica.
|
||||
- Sem regressão no `/campanha` canônico (auth WebAuthn/invite/same-origin mutação e o PWA).
|
||||
- **O que reavaliar:** o deliverable de CÓDIGO é só eliminar/atualizar cada referência in-repo a `pt.jorgesolla.com.br` para que nada do repo aponte/enconde a URL antiga como verdade. Desativação de DNS/hosting é INFRA (fora do app). Não há URL antiga em runtime não-test (o app é 100% canônico via env) — o trabalho é limpar fixtures/verbosidade/operação de default e personas que ainda citam a fonte antiga.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
**Opções consideradas:** A | B | C
|
||||
|
||||
**Recomendação:** **B — troca textual pontual da URL antiga → canônica no (A) código/testes/operacional e (C) personas de agente; docs históricos (B) intocados; referências de conteúdo `jorgesolla.com.br` (sem pt/1313) intocadas; fallback `next.config.mjs`/`apoiadores` intocado.** A intenção é narrow e bem definida; o risco está em sobrescopar. Manter docs como história (37signals: build less) e não tocar em fonte de conteúdo controla o diff ao mínimo correto.
|
||||
|
||||
**Rejeitadas:**
|
||||
|
||||
- A (alterar também docs históricos de ops): as impl-plans `ops50*`, `ops51*`, `ops52-*-impl`, `c98-impl`, `c115-impl`, `push-notificacoes-impl`, `revalidate-secret-vercel-production-impl`, `ci-deploy-cooldown-15min`, `CUTOVER-MAIN-ONLY` documentam o estado passado (são "history, not locks" — a própria intenção diz). Editá-los não muda runtime e suja o diff; recomendo deixar como história. `docs/CHANGELOG-AGENTS.md` é append-only e NUNCA editado à mão — fora de qualquer hipótese.
|
||||
- C (reaproveitar Vercel/preview host como canônico): o host de preview `teqo-git-b40-solla.vercel.app` no teste WebAuthn é o caso de DENIAL intencional (host distinto do configurado) — preservar.
|
||||
- D (tocar `next.config.mjs` L4 / `apoiadores/[id]/page.tsx` L82): esses defaults `https://jorgesolla.com.br` (minúsculo, sem pt/1313) são a base/fallback da origem WordPress para share-metadata de conteúdo — NÃO é a URL antiga do app (`pt.jorgesolla.com.br`). Alterá-los risca alterar o comportamento de links públicos apoiadores/posts sem benefício para o cutoff. Fora de escopo. (Se um produto futuro unificar o domínio canônico no share-metadata, isso é outra issue.)
|
||||
- E (`jorgesolla.com.br` como CONTEÚDO): seed/recover-media/CalendarFeed `UID:...@teqo.jorgesolla.com.br` wpArticles são refs de FONTE DE CONTEÚDO, não a URL antiga — nenhum toque.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- `scripts/check-push-chain.mjs`:
|
||||
- L13 (usage comment) `[--site https://pt.jorgesolla.com.br]` → `https://jorgesolla1313.com.br`.
|
||||
- L47 default `'https://pt.jorgesolla.com.br'` → `'https://jorgesolla1313.com.br'` (operational default real da ferramenta — é a única mudança que afeta runtime/verdicto por omissão).
|
||||
- L49 erro de exemplo `https://pt.jorgesolla.com.br` → canônico.
|
||||
- L15-16 e L85: prosa operacional stale (Neon unpooled / `vercel env pull`) → semântica homeserver (`~/stack/teqo-1313.env`, `DATABASE_URL` do `teqo_1313`). Nota-chave: a mensagem de erro já texto falls no homeserver (L49-50 do bloco `die`); alinhar o texto prosa restante à mesma semântica.
|
||||
- `tests/unit/campaignWebAuthnConfig.unit.spec.ts`: `configuredURL`/`forwardedHost`/`rpID`/`origin` nos casos prod (L17,18,21,32) e descrições/nomes de teste ("canonical HTTPS origin in production", "canonical origin here", "a production build is served there") → `jorgesolla1313.com.br`. **Preservar** o caso preview (L33 `teqo-git-b40-solla.vercel.app`) — é o DENIAL por host distinto (semântica intacta: canônico configurado vs host servido `*.vercel.app` → `toBeNull()`).
|
||||
- `tests/unit/campaignJsonMutationRoute.unit.spec.ts`: L27 (`new Request('https://pt.jorgesolla.com.br/...')`) e L54 (`Origin: 'https://pt.jorgesolla.com.br'`) → canônico. O teste de aceite de Origin (mesmo host) e o de cross-origin (evil.example) mantêm semântica.
|
||||
- `tests/unit/aiMarkdownLinks.unit.spec.ts` L66: `'https://pt.jorgesolla.com.br/campanha'` → `'https://jorgesolla1313.com.br/campanha'` (assert `isCampaignInternalLink === false` preservada — external URL continua false).
|
||||
- `tests/int/campaignMultiPhones.int.spec.ts` L336: `inviteUrl: 'https://pt.jorgesolla.com.br/campanha/convite/abc'` → canônico. É string plana concatenada no link WhatsApp (url de teste) — semântica irrelevante, troca segura.
|
||||
- `.opencode/agent/designer-campanha-solla.md`: L9 ("hoje em `pt.jorgesolla.com.br`") → canônico; L21 (lista de fontes de notícia) → canônico; L54 ("deploy na Vercel ... fonctions em gru1 ... x-vercel-id") — duplamente stale → operação no homeserver (sem Vercel).
|
||||
- `.opencode/agent/solla-comunicacao.md`: L19 (fonte oficial de notícias) → canônico.
|
||||
- `.opencode/skills/solla-comunicacao/SKILL.md`: L15 (e perfil L28 se presente) → canônico.
|
||||
- Novos docs do PR (não são "alteração"): `docs/plans/ops80-cutoff-desativar-url-antiga.md` (intenção), este impl, + entrada curta de changelog em `docs/changelog/<data>-ops80.md` (rodar `pnpm changelog:build` — agregado insert-only, mantendo append-only do CHANGELOG-AGENTS intacto).
|
||||
|
||||
### Dados → forma (se aplicável)
|
||||
|
||||
- Sem migração, sem schema, sem acesso/Consent/UI. Nenhuma coleção/global/field alterada.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Blindagem/estado-base**: `git status` limpo no worktree; confirmar ausência de `vercel.json`/`.vercel`; `rg -n "pt.jorgesolla.com.br" --hidden` para inventário completo da superfície antes de editar (garantir que nada fora da lista do explorer precisa de olhar).
|
||||
2. **Operação (A)**: editar `scripts/check-push-chain.mjs` (default `--site`, usage, erro, prosa homeserver).
|
||||
3. **Testes unit (A)**: editar fixtures/assertions de `campaignWebAuthnConfig`, `campaignJsonMutationRoute`, `aiMarkdownLinks`.
|
||||
4. **Teste int (A)**: editar fixture `inviteUrl` de `campaignMultiPhones`.
|
||||
5. **Personas (C)**: editar `designer-campanha-solla.md`, `solla-comunicacao.md`, `solla-comunicacao/SKILL.md`.
|
||||
6. **Doc delivrables**: criar `docs/plans/ops80-cutoff-desativar-url-antiga.md` + impl deste plano + entrada de changelog; rodar `pnpm changelog:build`.
|
||||
7. **Gate completo**: `pnpm exec tsc --noEmit`; `pnpm lint`; `pnpm format:check`; `pnpm exec knip`; `pnpm check:cycles`; testes afetados pinados por arquivo (`vitest run tests/unit/campaignWebAuthnConfig.unit.spec.ts tests/unit/campaignJsonMutationRoute.unit.spec.ts tests/unit/aiMarkdownLinks.unit.spec.ts`; int: `tests/int/campaignMultiPhones.int.spec.ts`); `pnpm build`. Revarredura final `rg -n "pt.jorgesolla.com.br"` deve retornar zero (excepto, se decidido, docs históricos — ver aceite).
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- `jorgesolla.com.br` (sem `pt`/`1313`) em seed/recover-media/wpArticles/`UID:...@teo.jorgesolla.com.br` e no share-metadata: FONTE DE CONTEÚDO/gênero, não a URL antiga — proibido tocar.
|
||||
- Fallback `next.config.mjs` L4 / `apoiadores/[id]/page.tsx` L82: base WordPress-doc, não o app antigo — fora de escopo (recomendado).
|
||||
- Hosts `*.vercel.app` em teste WebAuthn: caso intencional de denial por preview — preservar.
|
||||
- Migração de dados (OPS79) e redirect (OPS81): entregas futuras, não neste item.
|
||||
- Procurar "pt.jorgesolla.com.br" em binários/build/`pnpm-lock`: não deve existir; não caçar em `node_modules`/`.next` (gerados, fora).
|
||||
- Docs históricos e `CHANGELOG-AGENTS.md` append-only: não editar.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Risco**: troca acidental do caso WebAuthn de denial (mexer no fixture preview `teqo-git-b40-solla.vercel.app` / na comparação de host) quebraria a proteção `toBeNull()`. → Mitigação: editar APENAS `configuredURL`/RP canônico; o host preview permanece `*.vercel.app`; rodar o spec pinado e confirmar que o teste "refuses a Vercel preview" continua 0.
|
||||
- **Risco**: default `--site` do `check-push-chain` muda o alvo da única verificação operacional; se debugger rodar com omissão, passa a testar o canônico real. → Desejado (é o objetivo: canônico é a verdade). Nada a mitigar além de rodar o script `--site` explícito pós-mudança contra o canônico para validar o GET.
|
||||
- **Risco**: sobrescopar (mexer em docs/fallback/fonte-de-conteúdo) inflando o diff. → Mitigação: fases 1 e 7 com `rg` full darão inventário exato e confirmarão zero menções remanescentes fora do que se decidiu preservar.
|
||||
- **Risco**: `format:check`/`lint` falharem em arquivos .md de persona (formater/line-length). → Mitigação: editar preservando a formatação existente das linhas; rodar `pnpm format:check`/`pnpm lint` nos gates.
|
||||
- **Risco**: changelog inserir entrada duplicada/incompleta. → `changelog:build` é insert-only agregado; seguir o formato de um histórico recente e validar a saída do build.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] `rg -n "pt.jorgesolla.com.br"` (fora de `node_modules`, `.next`, docs históricos decididos-preservados) = 0; nenhuma nova referência à URL antiga introduzida.
|
||||
- [ ] `scripts/check-push-chain.mjs` default `--site`, usage e erro apontam para `jorgesolla1313.com.br`; prosa `vercel env pull`/Neon substituída por semântica homeserver (`~/stack/teqo-1313.env`).
|
||||
- [ ] WebAuthn spec: `configuredURL`/`rpID`/`origin` prod = canônico; caso preview `teqo-git-b40-solla.vercel.app` → `toBeNull()` preservado (teste "refuses a Vercel preview" verde).
|
||||
- [ ] `campaignJsonMutationRoute` (Request + Origin controlado), `aiMarkdownLinks` (external URL → false), `campaignMultiPhones` (`inviteUrl`) = canônico, semânticas preservadas.
|
||||
- [ ] Personas `.opencode/agent/designer-campanha-solla.md`, `.opencode/agent/solla-comunicacao.md`, `.opencode/skills/solla-comunicacao/SKILL.md` = canônico + homeserver; prosa "deploy na Vercel/functions gru1/x-vercel-id" removida/atualizada.
|
||||
- [ ] Docs novos do PR presentes (`ops80-cutoff-desativar-url-antiga.md`, este impl, entrada de changelog) + `pnpm changelog:build` ok; `docs/CHANGELOG-AGENTS.md` intocado.
|
||||
- [ ] Gates verdes: `tsc --noEmit`, `lint`, `format:check`, `knip`, `check:cycles`, specs unit/int pinados por arquivo, `build`.
|
||||
- [ ] Nenhuma migração, nenhuma mudança de acesso/Consent/schema/UI; fallback `next.config.mjs`/`apoiadores` e refs de conteúdo `jorgesolla.com.br` intocados.
|
||||
71
docs/plans/ops80-cutoff-desativar-url-antiga.md
Normal file
71
docs/plans/ops80-cutoff-desativar-url-antiga.md
Normal 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`.
|
||||
|
|
@ -17,16 +17,53 @@
|
|||
"models": {
|
||||
"deepseek-v4-flash": {
|
||||
"name": "DeepSeek V4 Flash",
|
||||
"reasoning": true,
|
||||
"interleaved": {
|
||||
"field": "reasoning_content"
|
||||
},
|
||||
"options": {
|
||||
"thinking": {
|
||||
"type": "enabled"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"context": 1000000,
|
||||
"output": 384000
|
||||
},
|
||||
"variants": {
|
||||
"low": {
|
||||
"reasoningEffort": "low"
|
||||
},
|
||||
"high": {
|
||||
"reasoningEffort": "high"
|
||||
},
|
||||
"max": {
|
||||
"reasoningEffort": "max",
|
||||
"textVerbosity": "low",
|
||||
"reasoningSummary": "auto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mimo-v2.5": {
|
||||
"name": "MiMo v2.5",
|
||||
"reasoning": true,
|
||||
"interleaved": {
|
||||
"field": "reasoning_content"
|
||||
},
|
||||
"limit": {
|
||||
"context": 1000000,
|
||||
"output": 128000
|
||||
},
|
||||
"variants": {
|
||||
"low": {
|
||||
"reasoningEffort": "low"
|
||||
},
|
||||
"high": {
|
||||
"reasoningEffort": "high"
|
||||
},
|
||||
"max": {
|
||||
"reasoningEffort": "max"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
* - The site URL is only fetched with GET.
|
||||
*
|
||||
* Usage:
|
||||
* PROD_DATABASE_URL=postgres://... pnpm db:check:push-chain [--site https://pt.jorgesolla.com.br]
|
||||
* PROD_DATABASE_URL=postgres://... pnpm db:check:push-chain [--site https://jorgesolla1313.com.br]
|
||||
*
|
||||
* PROD_DATABASE_URL should be the UNPOOLED Neon connection string (the
|
||||
* DATABASE_URL_UNPOOLED value from `vercel env pull --environment=production`).
|
||||
* The VAPID env check reads process.env — source them the same way, or run the
|
||||
* check inside an environment that already has them.
|
||||
* PROD_DATABASE_URL should be the connection string from the homeserver stack
|
||||
* (`~/stack/teqo-1313.env`). The VAPID env check reads process.env — source
|
||||
* them the same way, or run the check inside an environment that already has
|
||||
* them.
|
||||
*/
|
||||
import { Client } from 'pg'
|
||||
|
||||
|
|
@ -44,9 +44,9 @@ if (LOCAL_HOSTS.has(new URL(PROD_DATABASE_URL).hostname)) {
|
|||
let site
|
||||
try {
|
||||
const siteArg = process.argv.find((arg) => arg.startsWith('--site='))
|
||||
site = new URL(siteArg?.slice('--site='.length) || 'https://pt.jorgesolla.com.br').origin
|
||||
site = new URL(siteArg?.slice('--site='.length) || 'https://jorgesolla1313.com.br').origin
|
||||
} catch {
|
||||
die('--site=<url> deve ser uma URL válida (ex.: https://pt.jorgesolla.com.br).')
|
||||
die('--site=<url> deve ser uma URL válida (ex.: https://jorgesolla1313.com.br).')
|
||||
}
|
||||
|
||||
const results = []
|
||||
|
|
@ -82,7 +82,7 @@ try {
|
|||
|
||||
// --- Elo 2: VAPID envs (server + public) --------------------------------------
|
||||
// Proves the DIAGNOSTIC environment, not the deployment — source them from
|
||||
// `vercel env pull --environment=production` to mirror the deployed server.
|
||||
// `~/stack/teqo-1313.env` on the homeserver to mirror the deployed server.
|
||||
const vapidServer = process.env.VAPID_PUBLIC_KEY?.trim()
|
||||
const vapidPrivate = process.env.VAPID_PRIVATE_KEY?.trim()
|
||||
const vapidPublic = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY?.trim()
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ import {
|
|||
createSupporterRecord,
|
||||
updateSupporterContactRecord,
|
||||
} from '@/app/(campaign)/campanha/actions/supporter'
|
||||
import { SUPPORTER_REGISTRATION_CONSENT_KEY } from '@/lib/campaignConsentKeys'
|
||||
import {
|
||||
CAMPAIGN_INVITE_CONSENT_KEY,
|
||||
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
||||
} from '@/lib/campaignConsentKeys'
|
||||
import {
|
||||
BRAZILIAN_PHONE_DUPLICATE_MESSAGE,
|
||||
BRAZILIAN_PHONE_INVALID_MESSAGE,
|
||||
|
|
@ -41,6 +44,7 @@ import {
|
|||
import config from '@/payload.config'
|
||||
import { toSupporterDetailViewModel } from '@/utilities/supporter/supporterViewModels'
|
||||
import {
|
||||
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
|
||||
ensureLeasedConsent,
|
||||
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
||||
} from '../helpers/testDatabaseLease'
|
||||
|
|
@ -333,13 +337,17 @@ describe('C112 — busca e convite com números secundários', () => {
|
|||
phone: primary,
|
||||
recipientName: 'Liderança',
|
||||
senderName: 'Coordenação',
|
||||
inviteUrl: 'https://pt.jorgesolla.com.br/campanha/convite/abc',
|
||||
inviteUrl: 'https://jorgesolla1313.com.br/campanha/convite/abc',
|
||||
kind: 'autopreenchimento',
|
||||
})
|
||||
expect(url).toContain(`wa.me/55${primary}`)
|
||||
expect(url).not.toContain(`wa.me/55${secondary}`)
|
||||
|
||||
// And the invitation creation gate uses the ficha's primary.
|
||||
await ensureLeasedConsent(payload, {
|
||||
consentKey: CAMPAIGN_INVITE_CONSENT_KEY,
|
||||
leaseKey: CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
|
||||
})
|
||||
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
|
||||
const municipality = await campaignFixtures().getMunicipality()
|
||||
const created = await createLeadershipRecord(payload, coordinator, {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,22 @@ describe('setGoogleCalendarSyncDisabled action (S18/D7)', () => {
|
|||
|
||||
it('no-op without a config doc keeps ok with the derived not-configured state', async () => {
|
||||
await installStaffActor()
|
||||
// Ensure no residual config doc from parallel specs (C114 shares the test DB).
|
||||
const stale = await payload.find({
|
||||
collection: 'googleCalendarSync',
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
depth: 0,
|
||||
where: { calendarId: { exists: true } },
|
||||
overrideAccess: true,
|
||||
})
|
||||
if (stale.docs[0]) {
|
||||
await payload.delete({
|
||||
collection: 'googleCalendarSync',
|
||||
id: stale.docs[0].id,
|
||||
overrideAccess: true,
|
||||
})
|
||||
}
|
||||
|
||||
const result: GoogleCalendarSyncActionResult = await withGoogleCalendarTestCredential(() =>
|
||||
setGoogleCalendarSyncDisabled(true),
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ describe('isCampaignInternalLink', () => {
|
|||
it('rejects look-alike paths, web URLs and non-web protocols', () => {
|
||||
expect(isCampaignInternalLink('/campanha-mobile')).toBe(false)
|
||||
expect(isCampaignInternalLink('/campanhaX')).toBe(false)
|
||||
expect(isCampaignInternalLink('https://pt.jorgesolla.com.br/campanha')).toBe(false)
|
||||
expect(isCampaignInternalLink('https://jorgesolla1313.com.br/campanha')).toBe(false)
|
||||
expect(isCampaignInternalLink('mailto:contato@jorgesolla.com.br')).toBe(false)
|
||||
expect(isCampaignInternalLink('')).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const buildRoute = (handler: (body: { municipalityId: number }) => Promise<NextR
|
|||
)
|
||||
|
||||
const postRequest = (body: unknown, headers?: HeadersInit) =>
|
||||
new Request('https://pt.jorgesolla.com.br/campanha/municipios/political-trend', {
|
||||
new Request('https://jorgesolla1313.com.br/campanha/municipios/political-trend', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...headers },
|
||||
body: typeof body === 'string' ? body : JSON.stringify(body),
|
||||
|
|
@ -51,7 +51,7 @@ describe('campaignJsonMutationRoute', () => {
|
|||
)
|
||||
|
||||
const response = await route(
|
||||
postRequest({ municipalityId: 7 }, { Origin: 'https://pt.jorgesolla.com.br' }),
|
||||
postRequest({ municipalityId: 7 }, { Origin: 'https://jorgesolla1313.com.br' }),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ describe('getCampaignWebAuthnRelyingParty', () => {
|
|||
expect(
|
||||
getCampaignWebAuthnRelyingParty({
|
||||
environment: 'production',
|
||||
configuredURL: 'https://pt.jorgesolla.com.br',
|
||||
forwardedHost: 'pt.jorgesolla.com.br',
|
||||
configuredURL: 'https://jorgesolla1313.com.br',
|
||||
forwardedHost: 'jorgesolla1313.com.br',
|
||||
forwardedProto: 'https',
|
||||
}),
|
||||
).toMatchObject({ rpID: 'pt.jorgesolla.com.br', origin: 'https://pt.jorgesolla.com.br' })
|
||||
).toMatchObject({ rpID: 'jorgesolla1313.com.br', origin: 'https://jorgesolla1313.com.br' })
|
||||
})
|
||||
|
||||
it('refuses a Vercel preview, where the served host is not the configured one', () => {
|
||||
|
|
@ -29,7 +29,7 @@ describe('getCampaignWebAuthnRelyingParty', () => {
|
|||
expect(
|
||||
getCampaignWebAuthnRelyingParty({
|
||||
environment: 'production',
|
||||
configuredURL: 'https://pt.jorgesolla.com.br',
|
||||
configuredURL: 'https://jorgesolla1313.com.br',
|
||||
forwardedHost: 'teqo-git-b40-solla.vercel.app',
|
||||
forwardedProto: 'https',
|
||||
}),
|
||||
|
|
@ -49,7 +49,7 @@ describe('getCampaignWebAuthnRelyingParty', () => {
|
|||
it('follows localhost when a production build is served there (CI e2e)', () => {
|
||||
expect(
|
||||
getCampaignWebAuthnRelyingParty({
|
||||
configuredURL: 'https://pt.jorgesolla.com.br',
|
||||
configuredURL: 'https://jorgesolla1313.com.br',
|
||||
forwardedHost: 'localhost:3000',
|
||||
}),
|
||||
).toMatchObject({ rpID: 'localhost', origin: 'http://localhost:3000' })
|
||||
|
|
|
|||
Loading…
Reference in a new issue