Record the B40 closing triage without opening an ID.
Nothing left from the reviewers scored high enough to earn a roadmap item, so the four discards are written down with their reasons instead — a discard whose reason is missing comes back next session dressed as an obvious win. The one worth keeping is the enrollment toast's third spelling of the failure copy: it is deliberate, because that toast has no retry affordance and ending the sentence in "Tente novamente." would be a false instruction. Three engineering deferrals gained the trigger they lacked (shared HMAC core, an optional `bodySchema` on the route shell, promoting `postJson`) — all three are second-call-site abstractions, so the trigger is the third. Also corrects two statements this delivery falsified: the notebook still said `postJson` translates `SyntaxError`, which the simplify pass deleted, and the roadmap's B40 item carried the pre-optimization 130 kB that its own header line had already restated as 126 kB.
This commit is contained in:
parent
2cfb7d5e67
commit
fb64e36537
3 changed files with 14 additions and 2 deletions
File diff suppressed because one or more lines are too long
|
|
@ -118,6 +118,18 @@ Depth check: reusa `setCampaignAuthCookie` / `getCampaignUser`; não cria segund
|
|||
|
||||
- **Passkeys sincronizadas multi-device como copy de produto.** Revisitar quando: mesa pedir login no laptop novo sem senha após enrollment no celular.
|
||||
- **Exigir re-senha a cada N dias mesmo com biometria.** Revisitar se assessoria jurídica pedir após lote Onda 0.
|
||||
- **Núcleo HMAC compartilhado com `supporterImportToken.ts`.** ~35 linhas idênticas (assinar, comparar em tempo constante, `base64url`), 2 call sites. Revisitar quando: um **terceiro** consumidor de cookie/token assinado nascer — extrair agora mexeria no caminho do token de import por nada.
|
||||
- **`bodySchema` opcional em `campaignJsonMutationRoute`.** Duas das quatro rotas WebAuthn não têm corpo para validar e usam `campaignWebAuthnNoBodySchema` (`z.unknown()`), porque a casca sempre faz `parse`. Revisitar quando: nascer uma **terceira** rota JSON sem corpo fora da biometria — aí o `{}` que o cliente posta e o schema-fantasma valem um seam.
|
||||
- **`postJson` promovido a `campaignJsonRequest.ts`.** A casca de uma linha sobre `postCampaignJson` só descarta o `ok` (o discriminante do envelope é que decide), e é o único chamador que quer isso. Revisitar quando: um segundo módulo fora da biometria precisar do payload sem o `ok`.
|
||||
|
||||
## Explicitamente fora (triage do fechamento)
|
||||
|
||||
Achados dos revisores do `/simplify` **descartados com motivo**, para não voltarem como “óbvios”:
|
||||
|
||||
- **Sobrepor o `import()` do módulo de cerimônia ao POST de `*-options`.** Economiza um RTT **só na primeira** cerimônia do dispositivo (o chunk tem 3,7 kB gzip e fica em cache depois), nunca foi medido em ms, e threading do `Promise.all` por duas funções exportadas e três ilhas é mais código do que o ganho justifica.
|
||||
- **Webpack duplicando o módulo de cerimônia em dois chunks lazy (~1,6 kB gzip).** Dimensionamento de split-chunk, não coisa do código-fonte; o próprio revisor de performance o pôs abaixo de P2.
|
||||
- **A terceira grafia da falha de enrollment em `BiometricEnrollmentToast`.** “Não foi possível cadastrar este aparelho agora.” **não** é a constante compartilhada de propósito: o toast não tem afordância de repetir, então terminar em “Tente novamente.” seria uma instrução falsa. Unificar pioraria a copy.
|
||||
- **Unit test para `campaignWebAuthnClient`.** O único ramo que merecia pino — a tradução de `SyntaxError` — foi **deletado** neste fechamento; o que sobrou é transporte, coberto pelo e2e com autenticador virtual.
|
||||
|
||||
## Referências
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re
|
|||
- **B18** filtros salvos na lista de municípios — nomear o estado URL atual (`localStorage`); **Pass 2 W1 deixou trivial:** serialização canônica pronta e congelada em `municipalityListUrl.ts`; acesso rápido de 2º nível sob Municípios no sidebar (hover desktop / expand mobile; sticky só no filtro salvo ativo) · ~1–1,5d · Janela 1–2 · soft: B16/B17 (barra) · cortável · [plano](plans/filtros-salvos-municipios.md)
|
||||
- ~~**B19** gerenciar assessores — `/campanha/assessores` (lista + novo + detalhe): criar/editar contas `advisor`, ver/atribuir municípios, reenviar link de senha; **só Coordenador Geral e Candidato** (`isCampaignUnrestricted`); alinha access de update/phone do `candidate`~~ — **entregue 2026-07-24** (também alinha atribuição `municipality.advisors` a unrestricted; e-mail na lista via leitura privilegiada; auto-save da carteira) · [plano](plans/gerenciar-assessores.md)
|
||||
- ~~**B39** lembrar de mim / sessão longa em `/campanha`~~ — **entregue 2026-07-27** (checkbox default off; 8 h padrão / 14 d opt-in; `CampaignUser` configura o teto longo e `setCampaignAuthCookie` re-assina JWT + cookie para o TTL escolhido; `logoutCampaign` revoga o `sid` no servidor antes de limpar cookie/PWA; smoke E2E mede a expiração longa; sem migration/Consent) · [plano](plans/lembrar-de-mim-sessao-campanha.md)
|
||||
- ~~**B40** login com biometria (WebAuthn) em `/campanha`~~ — **entregue 2026-07-28** (enrollment opt-in por toast dismissível no shell + seção permanente em `/campanha/perfil`, botão de platform authenticator na tela de login emitindo o mesmo `campaign-token` de 14 dias, revogação por dispositivo; **vale para todos os papéis, liderança inclusive**, que é quem tem mais atrito; **sem `Consent`** — nenhum dado biométrico sai do aparelho, guardamos só uma chave pública, então o art. 11 não entra e o lote da Onda 0 não reabre. O Payload 3.82.0 **não tem caminho password-less**: o mint usa `payload.db.findOne` (nunca `findByID` — o `afterRead` `removePrivateAuthFields` apagaria `sessions`/`email` e o `addSessionToUser`, que reescreve o documento inteiro, derrubaria as outras sessões do usuário) + `checkLoginPermission` + `addSessionToUser` + `getFieldsToSign`/`jwtSign`, sob lock `campaign-webauthn-session:{id}` numa transação, com int spec provando que a sessão pré-existente sobrevive. Duas descobertas que a auditoria não previu: o RP derivado da origem canônica **tinha sucesso** num preview da Vercel e estourava `SecurityError` no browser, então a resolução agora exige que o host do request bata com o host derivado e devolve `null` quando divergem; e o challenge tem de ser **o mesmo bytes** que o `@simplewebauthn/server` gerou — a primeira versão gerava o seu e todo `register` respondia 400. Deleção de `campaignUser` ganhou `beforeDelete` em cascata (a FK que o Payload emite é `ON DELETE SET NULL` sobre coluna `NOT NULL`). Migration `20260728_041958_add_campaign_webauthn_credentials`; `@simplewebauthn/browser` custa 3,0 kB gzip e `/campanha/login` fica em 130 kB de First Load JS; e2e com autenticador virtual por CDP) · [plano](plans/login-biometrico-campanha.md)
|
||||
- ~~**B40** login com biometria (WebAuthn) em `/campanha`~~ — **entregue 2026-07-28** (enrollment opt-in por toast dismissível no shell + seção permanente em `/campanha/perfil`, botão de platform authenticator na tela de login emitindo o mesmo `campaign-token` de 14 dias, revogação por dispositivo; **vale para todos os papéis, liderança inclusive**, que é quem tem mais atrito; **sem `Consent`** — nenhum dado biométrico sai do aparelho, guardamos só uma chave pública, então o art. 11 não entra e o lote da Onda 0 não reabre. O Payload 3.82.0 **não tem caminho password-less**: o mint usa `payload.db.findOne` (nunca `findByID` — o `afterRead` `removePrivateAuthFields` apagaria `sessions`/`email` e o `addSessionToUser`, que reescreve o documento inteiro, derrubaria as outras sessões do usuário) + `checkLoginPermission` + `addSessionToUser` + `getFieldsToSign`/`jwtSign`, sob lock `campaign-webauthn-session:{id}` numa transação, com int spec provando que a sessão pré-existente sobrevive. Duas descobertas que a auditoria não previu: o RP derivado da origem canônica **tinha sucesso** num preview da Vercel e estourava `SecurityError` no browser, então a resolução agora exige que o host do request bata com o host derivado e devolve `null` quando divergem; e o challenge tem de ser **o mesmo bytes** que o `@simplewebauthn/server` gerou — a primeira versão gerava o seu e todo `register` respondia 400. Deleção de `campaignUser` ganhou `beforeDelete` em cascata (a FK que o Payload emite é `ON DELETE SET NULL` sobre coluna `NOT NULL`). Migration `20260728_041958_add_campaign_webauthn_credentials`; `@simplewebauthn/browser` custa 3,0 kB gzip e `/campanha/login` fica em 126 kB de First Load JS; e2e com autenticador virtual por CDP) · [plano](plans/login-biometrico-campanha.md)
|
||||
- ~~**B41** scroll horizontal + coluna Município fixa na lista de municípios~~ — **entregue 2026-07-27** (`overflow-x-auto` + `sticky left-0` na coluna nome, espelho **B21 ✓** `TerritoryList`; revisita o trade-off do **B16 ✓**) · [plano](plans/scroll-horizontal-lista-municipios.md)
|
||||
- ~~**B42** polimento mobile da lista de municípios~~ — **entregue 2026-07-27** (Tendência, Assessores **e Votos estimados** abrem Drawer no mobile e Popover no desktop através da casca compartilhada `CampaignCellEditOverlay`, extraída já na entrega porque o escopo virou 3 call sites; card mais baixo com Classe e Cobertura em paridade com a tabela — os props `layout` dos dois viraram dead code e morreram; "Abrir município" saiu e o card inteiro virou alvo de toque por link esticado, sem JS. O critique achou um **P0 no gesto central**: posicionar as células do `<dl>` em vez dos controles levantava rótulos e padding acima do overlay e transformava metade do card em toque morto — hoje só o trigger é `relative`, e o e2e toca justamente num rótulo dentro da grade. Outros três defeitos de comportamento entraram no mesmo passe: o Drawer focava um campo na abertura e subia o teclado virtual sobre a folha (`initialFocus` no título; o `preventContentAutoFocus` que devia mitigar isso era inerte no branch `sheet` — prop renomeado para `preventPopoverAutoFocus`), o `aria-label` do trigger anunciava o verbo e **engolia o valor** que o pill mostra (agora "Editar tendência em X — Favorável", que de passagem tirou `municipalityName` de payload RSC morto no desktop), e uma gravação que falhava **depois** do fechamento revertia o badge em silêncio, porque fechar desmonta o `Alert` que carregaria o erro (`toast.error` só quando já fechado, nos quatro controles de auto-save). `LeadershipListSupportStatusControl` (B32 ✓) era o branch Popover da casca redigitado à mão e virou o 4º call site; a classe do trigger, em três arquivos, virou `campaignCellEditTriggerClassName`. O critique rodou sobre o código: **evidência visual em device não foi coletada** — alvos de toque são checados por classe, não medidos) · [plano](plans/polimento-mobile-lista-municipios.md)
|
||||
- ~~**E18** documentação de conceitos de inteligência de campanha~~ — **entregue 2026-07-24** (`/campanha/conceitos` staff-only; 7 conceitos de E8 em `src/lib/campaignIntelligenceConcepts.ts` agrupados em base/diagnóstico/meta; "Saiba mais" por métrica nos tooltips + link de teclado no Popover do card; `formatElectionNumber` passou a arredondar votos) · cada fatia futura (E9/E10/B13/E11/E12/E13/E14) acrescenta sua seção ao array como parte da própria entrega · [plano](plans/documentacao-conceitos-campanha.md)
|
||||
|
|
|
|||
Loading…
Reference in a new issue