teqo-backup/docs/CHANGELOG-AGENTS.md
Francisco Solla 419ba39ecc
Merge pull request #639 from fsolla/B197-ocultar-a-coluna-de-e-mail-por-padrao-nas-listas-de-pes
B197 — Ocultar a coluna de e-mail por padrão nas listas de pessoas
2026-08-11 01:39:57 +00:00

24 KiB
Raw Blame History

Recently resolved (2026-08-10): Push /campanha — opt-in automático + fix do prompt nativo (ad-hoc). O requestPermission do card de perfil rodava dentro de startTransition(async …) — qualquer fronteira assíncrona entre o clique e a chamada derruba a user activation e o Chrome auto-nega silenciosamente (resolve 'denied' sem mostrar o prompt): o teste real em device deu exatamente "Permissão de notificação negada". Fix: os guards sync ficam no handler e Notification.requestPermission() passa a ser chamado sincronamente no clique (antes de qualquer await/transição); mensagens distintas para denied (permissão bloqueada — reativar nas configurações do site; um denied não pode ser re-perguntado por nenhuma API) vs default (prompt não exibido). Opt-in automático (pedido do usuário): novo CampaignPushOptInToast no shell (app) (padrão InstallPwaToast/BiometricEnrollmentToast), montado só para staff (leader não recebe push — sino idêntico), exibido ~2s após o login quando Notification.permission === 'default' (nunca denied), sem inscrição ativa no device, consentimento campanha-notificacoes-push publicado e VAPID configurado; iOS só com app instalado (standalone); "Aceitar e ativar" roda a mesma cerimônia com o requestPermission síncrono no clique do action; dismiss fica salvo em localStorage (chave campaign-push-opt-in-dismissed); ativação/desativação/leitura de inscrição extraídas para src/utilities/campaignPushClient.ts (compartilhado com o card — don't twin; registrado no pin de top-level com razão). Diagnóstico de prod (read-only, db:check:push-chain): consentimento campanha-notificacoes-push já publicado (PASS — nada a criar), envs VAPID presentes, SW versionado, 77 eventos no sino — único elo quebrado: 0 inscrições push (o erro de permissão impedia o cadastro). Sem migration/access/Consent/schema. Sem issue — entregue via ad-hoc no worktree work/3.

Recently resolved (2026-08-10): OPS37 (Issue #605) — o container Postgres compartilhado não é mais recriado por um worktree vizinho: pnpm db:start virou scripts/db-start.mjs (CLI) sobre scripts/lib/db-start.mjs — preflight "container teqo-postgres-1 Up + healthy/starting → no-op (compose nem roda); senão, compose -p teqo up -d --wait --wait-timeout 120 postgres como antes. Causa raiz provada por medição: o bind mount relativo ./docker/postgres/init do compose resolve um path absoluto por worktree → config_hash diferente por worktree (OPS37 39e89956… / B195 cf3cb522… / C101 917e11bb…; diff do config = só o source: do bind) → qualquer compose up cru de outro worktree fazia replace e matava dev+e2e de todos (terminating connection due to administrator command) — o incidente se repetiu ao vivo durante o gate: o worktree work/4 recriou o container no meio da suíte int (17 testes mortos; os 3 arquivos re-rodaram verdes e a suíte completa 691/691 passou em seguida). Verificação do aceite: StartedAt inalterado após db:start repetido, inclusive rodado de outro worktree (D9, hash diferente) — o caso que antes recriava. Recriação legítima (bump de imagem/config) segue por pnpm db:start --force-recreate (explícito; não testado live para não matar a run e2e do D9 em voo — flag é pass-through de 1 arg). worktree.mjs provisionamento delega para a mesma lib (mesmo risco no worktree next; dockerComposeUp/SHARED_COMPOSE_PROJECT órfãos removidos). Skip deliberado em starting (container que um peer acabou de criar): espera o boot ficar healthy (60s, polling 2s) em vez de recriar — e cai no compose up se travou; não skip em unhealthy/sem healthcheck (volta à config atual exatamente uma vez); db:stop/db:doctor/compose file/CI intactos. Unit novos (dbStart.unit.spec.ts, 17: matrix running×health + parse do inspect + wrapper mockado — skip não chama compose, argv do up/force-recreate, espera de starting). Sem migration/access/Consent/UI. Intenção container-postgres-compartilhado-compose-replace.md; impl container-postgres-compartilhado-compose-replace-impl.md.

Recently resolved (2026-08-10): OPS35+ (Issue #616) — restaurado o pin de browser que o OPS35 documentou como perdido: a visibilidade CSS real das colunas de rede (Assessor / Liderança / Dobradinha) e dos sortables de /campanha/territorios a 2200px. Spec novo mínimo tests/e2e/campaignTerritoriesColumns.e2e.spec.ts (projeto campaign, corpo exato do teste 3 do spec deletado — getByRole('columnheader') visível por rung container-query @min-[Nrem]/territory-list, Cobertura count 0); divisão de trabalho com o HTTP preservada (presença + classes de rung continuam no campaignTerritoriesHttp). Manifesto e2e-affected: entrada territórios agora mapeia os 2 specs e o arquivo dono das rungs TerritoryListColumns.tsx (uma mudança de rung acorda o pin; o seletor faz união, specs de municípios continuam acordando). Prova de não-vazio por mutação (rung advisor → @min-[200rem] fez o spec falhar em "Assessor"; revertido). Sem migration/access/Consent/UI/CSS. Intenção restaurar-pin-visibilidade-colunas-territorios.md; impl restaurar-pin-visibilidade-colunas-territorios-impl.md.

Recently resolved (2026-08-10): B197 (Issue #627) — coluna de e-mail oculta por padrão nas cinco listas de pessoas do /campanha (lideranças, dobradinhas, pessoas, apoiadores, assessores), religável pelo seletor de colunas B17 (cookie campaign_columns, default vale só para quem nunca tocou no seletor — escolha salva vence). Defaults novos em DEFAULT_HIDDEN_COLUMN_IDS (email nas 5) e assessores entrou em CAMPAIGN_LIST_IDS (primeira lista sem CampaignTable no mecanismo). Pessoas (C100 reaberto): o e-mail saiu da célula "Contato" (fica só telefone, — sem contato) e virou coluna própria oculta — cards mobile intocados (anti-goal). Apoiadores: coluna "E-mail" nova no SupporterList (dado já existia via contact; SupporterListItemViewModel ganhou email), nascendo oculta. Assessores: o encaixe — picker B17 na lista (trailing no omnibox, advisorPickerColumns espelhando os <th>) e AdvisorsTable (client) passou a receber columnVisibility com <th>/<td> condicionais; exceção única: a linha-draft de criação mantém o input de e-mail sempre (campo obrigatório no create). Sem migration/access/Consent/cache. Testes: unit do mecanismo (parser passa a guardar assessores; defaults + stored-wins) e e2e novo no campaignColumnPicker (default oculto, religar, persistência no reload, draft com input). Intenção ocultar-email-por-padrao-listas.md; impl ocultar-email-por-padrao-listas-impl.md.

Recently resolved (2026-08-10): B195 (Issue #591) — wizard "Registrar pedido" completo (A5, adiado no B85): o passo final saiu do stub e virou formulário funcional no shell do wizard — município no header do app (chrome municipalityLabel), sem título grande de passo (stepTitle={null} + contentFocus="none"), Tipo → Atividade (opcional) → campo único "O que você precisa?"; salvar ("Abrir demanda") → toast + retorno à origem (returnPath). Mudança de modelo da demanda: o cadastro (wizard e /demandas/nova, sem divergência) tem UM campo de texto (description required, min 2, max 4000 — constante CAMPAIGN_DEMAND_BODY_MAX_LENGTH); o título virou derivado por IA (src/utilities/ai/campaignDemandTitle.ts — generateText DeepSeek deepseek-v4-flash como o Sollinha, timeout 4s, sem key → null, prompt com rótulo do tipo) com fallback = texto truncado (src/lib/demandTitle.ts, puro: colapsa whitespace, ≤160) no create e keep-previous na edição (novo updateCampaignDemandRecord + editor inline no detalhe — recalc só quando a descrição muda; slug preservado pela invariante canônica); campaignDemandCreateSchema perdeu title (só os drafts de demanda da atividade mantêm título digitado — vertical fora de escopo, schema desacoplado). Unicidade de slug defensiva só no create das forms (ensureUniqueDemandSlug, opt-in context.campaignDemandUniqueSlug fail-closed em transação ativa — precedente skipContactPhoneInvariant): probe único da janela -2…-20 + advisory lock campaign-demand-slug:<base> contra race; activity drafts/admin mantêm a constraint estrita (contrato de rollback C90 pinado em teste). Page: branch register-demand no router + staff gate ANTES do load do escopo (gate: 'staff' → líder vai a /campanha/contatos, B43 — o 403 do scope load pré-existente virava erro não determinístico; stub WizardMunicipalitySelectedStub removido, placeholder "em breve" apagado). Bug latente corrigido no hook compartilhado useCampaignFormSuccessToast (dependência [state.message, state.status] → [state]: segundo save com a mesma mensagem — caso do editor — não disparava toast/refresh). Copy compartilhada em schemas/campaignDemand.ts; parser único parseCampaignDemandCreateFormData (utilities/demand/). Sem migration (título continua required, preenchido pelo record). Testes: unit do fallback/limites exatos; int (create deriva, colisão -2/-3, min 2, update keep-previous/skip, leader edit bloqueado, pin "sem DEEPSEEK_API_KEY" no beforeAll); e2e novo campaignRegisterDemand (fluxo completo com título na lista, back-nav à busca, lockdown de líder). Intenção registrar-pedido-wizard.md; impl registrar-pedido-wizard-impl.md.

Recently resolved (2026-08-10): OPS33 (Issue #595) — worktree next claima deterministicamente antes de criar o worktree: mesma fila/ordem e lock otimista do pnpm agent:claim, com o claim extraído para scripts/lib/agent-github.mjs (claimIssue — lock + flip + comentário; claimBriefLines — brief compartilhado; claimQueueEntry — shape para o reopen; claimTargetVerdict — decisão pura testada) e o agent-claim.mjs virou CLI fino com a mesma saída; claim falhou (fila vazia/race/--issue inválido) → motivo e saída sem worktree órfão (branch validada antes do claim; hint de falha pós-claim). --issue <N> claima a Issue direcionada (ready) ou reabre uma já claimada (in-progress — sem re-claim, idempotente; recupera também falha pós-claim); a saída imprime o brief e avisa "já claimada — NÃO rodar pnpm agent:claim". O launch do terminal agora envia /work-issue --issue <N> (prompt citado na diretiva) e a função worktree() de .agents/shell/worktree.sh passou a tokenizar a diretiva via xargs (quote-aware, nunca eval) — o valor do --prompt carrega espaço. --stay suprime cd+launch mas o claim acontece; o comando /worktree roda o mesmo script (claim incluso, sem launch). Guard -B contra reset de commits de sessão no reopen. Pool intacto (claimIssueForPool coordenado). Docs: AGENTS.md ("Per-worktree environments"), header/uso do script, .opencode/commands/worktree.md, skill worktree-next-issue (sai "read-only"), AGENT-OPS. Sem migration/dados/access/UI. Intenção worktree-next-claima-issue-deterministicamente.md; impl worktree-next-claima-issue-deterministicamente-impl.md.

Recently resolved (2026-08-10): D9 (Issue #596) — flake do consent restore do testDatabaseLease sob carga eliminado. Caracterização com reprodução (3 falhas em 8 runs da suíte int completa; depois isolada a 2 arquivos): o assertion "restores the exact configured consent after deletion and recreation" via leituras voláteis de uma linha canônica que escritores legítimos de outras specs deletavam/recriavam no meio do teste. Causa raiz em três camadas: (1) a janela de operação de withMissingInviteConsentFixture era a única transição de estado sem lease da suíte (outras specs criavam linha efêmera que o restore deletava sob elas); (2) removeOnda0ConsentAndPrivacyDb rodava sob leases shared e deletava as 4 keys estáveis no meio de testes alheios (o id restaurado sumia irreversivelmente); (3) asserções de restore comparavam contra leituras before que o provision Onda0 podia re-textar legitimamente. Fix (só testes, zero src/): janela de operação sob lease shared (serializeWindow, default true; o teste de ensures concorrentes migrou para key/lease privada — único caso que deliberadamente mantém janela sem lease, com re-verificação/re-delete do gap setup→janela; exclusivo descartado: alargava o ABBA com os escritores de invite sob carga); spec Onda0 com leases shared + down-migration restaura as linhas com os ids originais dentro da janela (restoreConsentRows compartilhado; no-op para o resto da suíte); asserções de restore referenciam o snapshot do fixture (contrato real), rollback asserta id+key; ORDER BY id/sort: 'id' nas leituras limit-1; core withDatabaseLease; ensureInviteConsent órfão removido (knip). Contraprova: suíte completa ×5 + par mínimo ×8 + 6 arquivos da key ×13/14 verdes (runs com restart externo do Postgres descartados por uptime). Intenção test-database-lease-consent-flake.md; impl test-database-lease-consent-flake-impl.md.

Recently resolved (2026-08-10): B193 (Issue #542) — card de município mobile denso + edit-where-you-see + última atualização expandível. O card de /campanha/municipios no mobile deixou o grid de labels e virou composição do wireframe: cabeçalho nome+território com posição de voto 2022 à direita (percentual em cima; colocação · votos embaixo), barra de cenários (pessimista·central·otimista + marcador do ativo — VoteEstimateScenarioStrip como trigger da sheet de votos existente), chips classe/tendência/nível (tendência e nível com label à esquerda, dentro do trigger — tocar no label abre a sheet; classe com razão via tooltip que abre no toque), 3 grupos de avatares com label (Assessores/Lideranças/Dobradinhas — um avatar por entidade, wrap, sem cap; leitura de assessores para não-coordenador), rodapé "Última atualização há X dias" + chevron que expande o limite inferior revelando o último card de atualização (badges + corpo + autor · data, sem moldura) + CTA "Registrar atualização" (sheet existente); sem atualização → o rodapé vira CTA direto (sem chevron/expansão); prioritário = borda lateral direita 6px border-primary no lugar do flag; Salvador read-only; "Cobertura" (conta da cadeira) sai do card (fica na tabela/detalhe — gate humano); rodapé mede a idade da atualização (não o frescor update∨pledge — coluna desktop intacta). C109 corrigida no caminho (a "sheet quebra ao abrir" era real): os callbacks dos controles (autosave onOpenChange, handleOpenChange do nível) são recriados por render — qualquer re-render com a sheet aberta (digitar no sheet!) fazia o host descartar a sessão e o corpo sumia; sessões do host agora são keyed por id de sessão (imune a identidade) e o footer customizado (submit + DialogClose) passou a ser registrado e renderizado pelo host dentro da árvore do Drawer (portal perdia o Dialog context — o e2e antigo só abria a sheet de tendência, sem footer custom, por isso a C109 não reproduzia). Controles ganharam prop trigger/triggerClassName (precedente RelationChipCell.trigger); MunicipalityRelationAvatarStack ganhou modo wrap. Dado: MunicipalityListViewModel.lastUpdate (batch por página via loadMunicipalityLastUpdates, scoped por portfólio). E2E novos (390×844): barra abre sheet e reflete valores salvos, chip Nível abre a sheet, borda de prioridade (6px + cor), expansão/CTA vazio. Sem migration/access/Consent. Intenção municipios-card-mobile-edit-where-you-see.md; impl municipios-card-mobile-edit-where-you-see-impl.md.

Recently resolved (2026-08-10): OPS28 (Issue #584) — worktree next/plan passam a aplicar o seed mínimo além das migrations em cada banco provisionado (migrate + db:seed:minimal por banco, dev e test) — paridade com a CI, que roda o mesmo par antes do e2e: o e2e de worktree deixa de divergir do de CI (specs dependentes dos pins priority: 'alta' de salvador-ze-1/camacari — FAB suggest e campaignSavedFilters — falhavam só localmente com banco migrado cru 435×normal, o padrão que o ledger TECH-DEBT row 19 ficha como "masks real failures"). --no-migrate pula migrate e seed (o seed depende do catálogo migrado); re-provisionamento continua idempotente (upserts). Fallback sem Docker (Cursor Cloud) também roda migrate+seed nos teqo/teqo_test compartilhados (decisão de produto do gate: paridade total). Guard de regressão tests/e2e/seedParity.setup.e2e.spec.ts no projeto setup do Playwright: falha cedo com mensagem clara ("rode pnpm migrate && pnpm db:seed:minimal") se o banco de teste não estiver seedado — dependentes do setup pulam, nada de spec vermelho confuso; a evidência checada são os users do seed (MINIMAL_CAMPAIGN_USERS), deliberadamente não os pins priority: 'alta' — o cleanup dos fixtures e2e reseta priority dos municípios que toca (round-robin por sequência persistente), então pins morrem legitimamente após runs repetidas no mesmo banco de worktree e só o pnpm worktree next re-aplica (guard nunca checaria pins); scripts/worktree.mjs entrou em HIGH_RISK_EXACT (mudança no provisionador roda a suíte cheia na CI). Sem migration/access/Consent/UI. Intenção worktree-seed-minimal-paridade-e2e-ci.md; impl worktree-seed-minimal-paridade-e2e-ci-impl.md.

Recently resolved (2026-08-10): OPS34 (Issue #599) — o job e2e do CI (o mais longo da cascata, 435 s medidos no run 31412425553: ~67 s fixos + build 172 s + testes 196 s) roda agora em 2 shards paralelos (--shard=N/2, matrix fail-fast: false, build por shard — opção A da intenção validada pela medição: por shard ≈ 5–5,5 min; artefato de build compartilhado rejeitado porque os shards só começam após o build → wall maior). Cadeia de projetos do Playwright (setup → campaign → frontend → admin) + prewarm do setup viraram dev-only: playwright.config.ts ganhou isProdMode = CI || E2E_PROD=1 (mesma condição do webServer.command, agora reusada) — em prod o projeto setup sai do array (arquivo não casado = não coletado) e as dependencies zeram (famílias em paralelo contra o build de produção, onde não há compile a preaquecer); dev intocado (provado: --list mostra [setup] só sem CI; smoke e2e dev rodou setup→campaignAuth). ci-pr.yml: o job scope emite e2e_matrix/e2e_shards via novo e2eShardConfig(mode) em scripts/lib/test-affected-core.mjs (fonte única, unit-pinada: full → [1,2]/2, selected/none → [1]/1 — pagar 2 builds por 3 specs é desperdício); ci.yml hardcodeia [1,2]/2 com comentário apontando o single-source. PLAYWRIGHT_WORKERS: 2 explícito no env dos jobs e2e (aceite: explícito, default local intocado). Armadilha registrada nos workflows: flags do playwright vão sem o separador -- — pnpm repassa o -- literal ao playwright e a flag vira file pattern ("No tests found"). Specs intactas (o setup não foi removido — virou dev-only, já documentado como prewarm dev-only); sem migration/access/Consent/UI. Medição pós-merge (main, run 31418206512): job e2e 435 s → 326 s (shard 1) / 369 s (shard 2) — shard 2 = fixo 68 s + build 176 s + testes 124 s (a fase de testes caiu de 196 s seriais para 124 s — metade da suite por runner + cadeia removida; o shard 2 ainda pagou retry da flake conhecida de campaignMunicipalities; shard 1: testes ~82 s); gate completo do pipeline (checks) ~7,5 → ~6,4 min. Build passou a dominar o shard — gatilho da intenção (opção B, artefato compartilhado) reavaliável quando os testes encolherem mais. Intenção ci-e2e-paralelizar-job.md; impl ci-e2e-paralelizar-job-impl.md. Recently resolved (2026-08-10): OPS35 (Issue #600) — modo de spec e2e full-stack sem browser (paradigma HTTP): mesma suíte (pnpm test:e2e), mesmo job/CI, mesmo servidor + DB reais, sem lançar navegador. Base nova tests/e2e/fixtures/campaignHttpTest.ts estende o test cru de @playwright/test (nunca e2eTest.ts — o guard e2eFailureGuard é auto e puxaria page/context), reusa o fixture campaign do browser (exportado como campaignFixture — ownership/cleanup in-process, zero caminho de seed paralelo) e adiciona o fixture campaignRequest(user, senha): login pelo REST real do Payload (POST /api/campaignUser/login, loginWithUsername aceita email/username) → request.newContext com o cookie canônico campaign-token (path /campanha, flags espelhando setCampaignAuthCookie; secure pelo scheme do baseURL, não pelo NODE_ENV do runner). Nomenclatura ajustada contra a letra da intenção (descoberta de execução): *.http.e2e.spec.ts não casa o testMatch /campaign.*\.e2e\.spec\.ts/ do projeto → campaign<Family>Http.e2e.spec.ts no mesmo projeto (dependência setup de prewarm já coberta). Primeira família migrada: campaignTerritories (4 specs campaignTerritoriesHttp.e2e.spec.ts, browser removido; manifesto e2e-affected atualizado — unit-pinned). Equivalências: clicks viravam GETs de URL (?sort=votes2022 → caption "Ordenado por 2022 (maior primeiro)"; ?region=Irecê → "1 território encontrado"); DOM virou HTML renderizado — com duas realidades de render registradas no header do spec: separadores RSC <!-- --> (asserções rodam sobre rendered(), comentários são transporte, não conteúdo) e o label "Cobertura" presente no flight payload (menu do picker é client component serializado mesmo fechado) — o pin "oculta por default" é a ausência do controle sortable, que resolveVisibleColumns não renderiza. RBAC leader: o redirect noLeader é redirect() pós-stream → o documento responde 200 com <meta http-equiv="refresh" content="1;url=/campanha/contatos"> (Next next-route-redirect), não 3xx — o meta é o contrato assertado (dev 307/prod 308 quando o redirect vence o stream; nunca pinar status numérico). Medição (dev server, worktree 3135, load ~68, prewarm incluído nas duas versões): família browser 56,0 s (22,9/17,5/7,9/7,7) vs HTTP 33,7 s (10,8/8,4/2,4/12,1) — −40%; o ganho é estrutural (sem login de browser, sem hidratação, sem waitForURL), a carga da máquina domina a variância. No prod build local (E2E_PROD=1, o modo do CI) a família HTTP fecha 4/4 em 3,1 s (1,4/0,9/0,5/0,3). Guard de falhas client permanece exclusivo do browser (aceite). Sem migration/access/Consent/UI/schema. Intenção e2e-full-stack-sem-browser.md; impl e2e-full-stack-sem-browser-impl.md.