From 3892c2e0a44684f5fa1fd15556cedc4da2646ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Mon, 27 Jul 2026 08:57:26 -0300 Subject: [PATCH] Add remembered campaign sessions Keep campaign JWT and cookie lifetimes aligned while revoking the active server session on logout. --- .cursor/rules/projects/nucleos-eleitorais.mdc | 1 + ...p-campaign-campanha-login-loginform-tsx.md | 60 +++++++++++ AGENTS.md | 6 +- docs/plans/lembrar-de-mim-sessao-campanha.md | 33 +++--- docs/roadmap.md | 4 +- src/app/(campaign)/campanha/actions/auth.ts | 33 ++++-- .../(campaign)/campanha/login/LoginForm.tsx | 26 ++++- src/collections/CampaignUser.ts | 2 + src/lib/campaignAuthCopy.ts | 3 + src/lib/campaignSessionTtl.ts | 5 + src/lib/schemas/campaign-login.ts | 1 + src/utilities/campaignAuth.ts | 102 +++++++++++++++--- tests/e2e/campaignAuth.e2e.spec.ts | 39 +++++++ tests/int/campaignAuth.int.spec.ts | 30 +++++- tests/unit/campaignAuthCookie.unit.spec.ts | 81 ++++++++++++++ .../campaignCompositionCleanup.unit.spec.ts | 17 +++ tests/unit/campaignLoginAction.unit.spec.ts | 86 ++++++++++++--- 17 files changed, 472 insertions(+), 57 deletions(-) create mode 100644 .impeccable/critique/2026-07-27T03-38-47Z__src-app-campaign-campanha-login-loginform-tsx.md create mode 100644 src/lib/campaignSessionTtl.ts create mode 100644 tests/e2e/campaignAuth.e2e.spec.ts create mode 100644 tests/unit/campaignAuthCookie.unit.spec.ts diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 8f2b5d2c..54d29e1f 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -11,6 +11,7 @@ alwaysApply: false - **B8+ entregue em código (2026-07-27):** os quatro débitos do `/simplify` do B8 F2, e o achado central mudou de dono. **F1 — a alavanca não era a que o plano dizia.** O plano tratava "escala de chão" como algo implementado via dígitos de quantização; medindo o re-snap do artefato commitado para grades mais grossas, quantização sozinha quase não corta: ~12 m → 8.990 pontos (99% do original), ~25 m → 8.164, ~50 m → 6.219, e **mesmo na grade da malha estadual (~103 m) → 3.849 pontos, ainda 203/feature** — ~9× o irmão. Quem manda é o **limiar absoluto de Visvalingam**, que o plano citava entre parênteses. Então a política nasceu invertida: o peso absoluto **em m² de chão** é a política, os dígitos são derivados do bbox de cada topologia. `scripts/lib/topology.mjs` (**primeiro módulo de `scripts/lib/` — o B5 F2 pousa nele, não abre um segundo**) expõe `SIMPLIFY_TOLERANCE_M2` por malha, `GROUND_GRID_METRES = 102.82`, `simplifyToGroundScale`, `quantizeToGroundGrid` e `describeGroundScale`; `SIMPLIFY_QUANTILE`/`QUANTIZE_DIGITS` morreram nos dois scripts. Resultado medido: as zonas caíram de **9.120 → 574 pontos de arco (480 → 30,2/feature) e 72.310 → 7.687 bytes**; `bahia-municipalities` saiu **byte-idêntico** (a política foi semeada no valor histórico dele); territórios mudou **1 byte** (a grade passou de 102,730 para 102,822 m ao ser derivada do bbox em vez de um `1e4` fixo). **Três fatos para ninguém re-derivar:** (1) tolerância é **por malha de propósito** — a municipal (1,78 km²) chega a 17,9 pontos/feature nas zonas, mas come **5% da área de terra de Salvador (306 → 291 km²)**, que é justamente a grandeza com que o int prova que nenhum bairro foi descartado nem duplicado; as zonas ficaram em 0,06 km² e os territórios em 13,6 km² (dissolve da malha municipal, já vêm generalizados); (2) **a ordem importa** — quantizar antes de simplificar colapsa vértices em duplicatas e o `merge` posterior produz anéis quebrados (medido: Salvador dissolvendo para 24 km² em vez de 307); (3) a área das zonas pós-corte é **306,3 km²** (era ~324), dentro da janela de 280–360 km² que o int já tinha. `MAX_TOPO_BYTES` ficou e ganhou ao lado **`MAX_POINTS_PER_FEATURE = 60`** — bytes só pegam malha que ninguém simplificou (as zonas viviam a 10% do teto de bytes com 22× a densidade do irmão — 26× é a razão do passo de grade, não da densidade). `zoneNumber` saiu do artefato e do tipo. **F2:** `featureMapKey` passou a rodar sobre features **reais** das três malhas, cada uma no laço que já a varre inteira — 417 `codarea`, 27 `code`, 19 `slug` (a zona responde slug mesmo carregando o `ibgeCode` da cidade); a primeira escrita asseria a cidade dentro do laço das zonas, e o `/simplify` mostrou que isso era **uma** feature municipal asserida 19 vezes (as 19 ZE compartilham o código de Salvador) enquanto 416 municípios e 27 TIs nunca passavam por `featureMapKey`; o caso de precedência do unit usava `{ municipalitySlug, codarea }`, forma que **nenhuma malha produz** — caiu, e o comentário passou a declarar o invariante real: as três propriedades de chave são **mutuamente exclusivas**, e `ibgeCode` na malha de zonas deliberadamente não é chave. **F3:** `.catch()` na metade das zonas — e a precisão que o plano deixava otimista: como o bundle é keyed por slug, **Salvador não volta a pintar agregada**, volta como polígono sem dado e sem interação (não está em `scopedKeys`); a memoização da rejeição fica (é o estado de falha acionável do B14) e `status === 'error'` segue reservado à malha municipal. A ressalva de aproximação saiu do gate `!comparisonActive` e virou `

` condicionado a `bundle.hasZoneMunicipalities` — campo novo do contrato, derivado de `kind === 'zona'` no loader. O `/simplify` rejeitou as duas alternativas: um `isZoneMapKey` no cliente (segunda codificação da regra de `mapKeyForMunicipality`, em espaço negativo) e `zoneBreakdown.length > 0` (recria o acoplamento que o fill-in da lista de zonas vai deletar); com o campo próprio, o fill-in só apaga o `