Commit graph

302 commits

Author SHA1 Message Date
Francisco Solla
5b7448832b ops(OPS73-FOLLOWUP): fecha #760 — resíduos da race do usuário compartilhado no e2e (lock único de login) + docs drift 2026-08-23 15:54:49 -03:00
Francisco Solla
f029c6f86e fix(S12): deep link #novidades aterrissa no formulário no mobile
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; no mobile o reflow do h-dvh + imagens eager do hero deslocam o layout
após o scroll inicial.

Novo componente cliente CampaignHashScroll (renderiza null) montado no
layout da home: retenta o hash inicial até a seção entrar no viewport
(parando na primeira interação do usuário) e ouve hashchange para âncoras
in-page. scrollIntoView sem behavior herda scroll-smooth/motion-reduce.

E2E novos: describe mobile (load direto, CTA, reduced-motion) + load direto
desktop em describe próprio (evita corrida no beforeAll de consent).
2026-08-23 14:27:15 -03:00
Francisco Solla
a74a2dca9e refactor(OPS46): simplify pós-revisão — release do claim ao fim do cleanup (janela purge segura), gate da hook exige o par de flags, Set→boolean, guard setval/nextval\s*\( e spec de steal invariante-primeiro 2026-08-23 14:24:33 -03:00
Francisco Solla
89bf9d0854 ops(OPS46): e2e local coleta via hook ESM (next/cache + server-only) e allocator de município com registry anti-wrap (Closes #733)
Fase 1: playwright bare (--list, extensão VS Code, runs ad-hoc) quebrava na
coleta — next@15 sem exports não resolve 'next/cache' em ESM nativo e
server-only exige a condição react-server; o script test:e2e carregava ambas
via NODE_OPTIONS (por isso o CI passava). Hook de resolução ESM vitest-free
(e2eEsmResolve.mjs) registrada no main via module.register + propagada aos
workers via NODE_OPTIONS quando invocada bare; path do script intocado.

Fase 2: a sequência persistente campaign_fixture_municipality_alloc nunca é
resetada e um ciclo completo claima ~505 do catálogo de 435 — o wrap em todo
ciclo em DB de worktree fazia dois runs concorrentes receberem o mesmo índice
e purgarem as linhas vivas um do outro. Registry de claims compartilhado
vitest-free (campaignMunicipalityAllocator.ts): INSERT ON CONFLICT DO NOTHING
com skip de slot vivo, steal de claim stale (TTL 2h), release no cleanup dos
dois fixtures, erro claro de exaustão. Guard estático no
testMunicipalityAllocatorConventions + spec int de stress.
2026-08-23 14:02:19 -03:00
Francisco Solla
0d483a08db feat(campaign): C142 UI respects advisor permission profile somente_leitura
- Pure helpers: advisorEditingScope, rowEditingAllowed in campaignAdvisorProfile.ts
- gate: 'writable' in requireCampaignPageActor for route-level enforcement
- Shared cells accept readOnly prop: CampaignInlineEditableCell, MunicipalityRelationEditor, PhonesFieldEditor, DemandDescriptionEditor, etc.
- Municipality pages compute editingScope via resolveMunicipalityWriteScope and thread it to list/detail/edit surfaces
- Home actions, FAB mount rule, quick actions registry accept editingScope parameter
- Demands, activities, supporters, organizations create buttons gated
- E2E spec: campaignPermissionProfile.e2e.spec.ts covering 3 profiles
- Unit tests: campaignAdvisorProfile, accessShared, campaignComponents updated
- gate:fast passed (tsc, lint, format, knip, cycles, unit)
2026-08-21 23:01:35 -03:00
Francisco Solla
c723d6accf
merge(OPS75): convergência Forgejo→GitHub — GitHub como fonte única
* deploy: limpeza pós-deploy do build cache e tags locais antigas (INF3/F2)

O deploy compila no homeserver e acumulava build cache + tags locais
antigas no disco raiz (incidente 19/08: 45G de cache). Após o smoke:
docker builder prune -f + remoção das tags locais teqo-1313 exceto o
SHA em uso (best-effort, registry preserva o rollback).

* feat(S9): captura de novidades da campanha na home pública (form + CTA do hero)

Seção 'Receba as novidades da campanha' acima do rodapé: nome + WhatsApp
obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado
'Quero fazer parte do time' gravado como subscription.campaignLevel
(time|esporadico). CTA secundário do hero vira o atalho (#novidades, scroll
suave). Contact + Subscription em withPayloadTransaction com consentimento
novo fail-closed por chave estável 'campanha-novidades' — nada é gravado sem o
texto aprovado no admin. Migration 20260819_192605 (state de contact nullable
+ coluna campaign_level). Impl plan, changelog, int + e2e.

* plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil

* plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil

* feat(S8): seção 'Nossa história' na home — vídeo da trajetória embutido do YouTube (Closes #91)

* docs(S8): resolução do conflito com S9 (história → newsletter) + nota do guard e2e dev-only

* feat(S10): pixel do Meta na página pública de campanha (campo admin em SiteSettings + PageView na home + Lead no sucesso da captura)

Campo tracking.facebookPixelId no global SiteSettings (mesma validação/normalização do precedente Petition, regra centralizada em facebookPixel.ts); home (grupo (home)) injeta o base code + PageView quando o ID está configurado (fail-closed) e o CampaignNewsletterForm dispara exatamente um Lead no sucesso do S9. Migration 20260819_213947_add_site_settings_facebook_pixel_id. Closes #94

* refactor(S10): simplify pass — Lead wiring unit-tested e raças de estado compartilhado do e2e resolvidas

e2e: stub hermético de connect.facebook.net no guard compartilhado (specs paralelos renderizam a home com o pixel); advisory lock no Postgres serializando o login do usuário de teste (Payload perde sessão em login concorrente read-modify-write — classe documentada no webauthn); seedTestUser vira create-if-missing (sem janela de usuário ausente nem reset de senha); Lead do form protegido do catch de erro (tracking nunca falha uma captura commitada); descrição do campo compartilhada; unidade nova campaignNewsletterLead cobre exatamente um Lead no sucesso.

* feat(C141): perfis de permissão por assessor — Visão × Edição configuráveis, enforcement fail-closed (Closes #105)

* chore(C141): regenera changelog agregado após rebase com S10

* plans(C144): options write-scoped para formulários de escrita pós-C141

---------

Co-authored-by: Francisco Solla <admin@solla.dev>
2026-08-21 01:41:18 -03:00
Francisco Solla
3ce5a69555 feat(S11): feed do Instagram configurado não aparece na home — painel de status da sincronização no admin
O fail-closed silencioso da S3 deixava a assessoria cega: token errado não deixava
rastro além do picker vazio. Fim da caixa-preta:

- `instagramSyncStatus` (jsonb hidden) na global `social-feed-settings`, escrito pelo
  render, pelo afterChange (sync awaited quando a credencial IG muda) e pela rota
  `POST /api/social-feed/sync` (botão, auth admin + same-origin).
- `InstagramApiError` + `describeInstagramError`: o fetch parseia o body de erro da
  Graph API e o mapeia para copy pt-BR com a correção (OAuthException/expired →
  "token inválido/expirado ou emitido via Facebook Login — gere pelo Instagram Login";
  "Invalid user id" → confira o ID; 5xx → indisponível; TypeError → rede; AbortError →
  demorou demais). O ID só casa com "user" E "id" juntos (token rejections real falam
  "the user must be logged in" — não podem virar a branch de ID).
- `InstagramSyncStatusPanel` (campo ui irmão do picker): 5 estados fiéis ao rascunho;
  gate respeita o master `enabled`; `outcome` transiente limpo ao editar credencial;
  aria-live nos boxes interativos. Home inalterada (fail-closed intacto).
- Bug real: `afterChange` roda dentro da transação do save (row lock até os hooks
  retornarem); persistir o status via pool numa 2ª conexão deadlockava no lock da
  própria linha (~80-230s/save). Fix: persists do sync-hook na MESMA transação via
  `getPostgresTransactionDatabase` (C6); render/rota sem transação seguem no pool.
  Diagrama: UPDATE transactionid Lock vs idle in transaction; fix verificado 45ms vs 232s.
- Stub IG ganha estado `invalid-token`; runbook docs/ops/instagram-feed-token-runbook.md;
  migration add_instagram_sync_status (jsonb nullable).
- Débitos registrados: #124 (janela de lock) e #125 (polish P3).
- Gates: unit 2463 + int 791; lint/format/tsc/cycles; build; e2e admin 5/5 + frontend serial 19/19.
2026-08-20 00:54:58 -03:00
Francisco Solla
2cc8f2c14d feat(C143): demandas visíveis apenas para responsáveis explícitos + candidato/coordenador
- campaignDemand.responsibles (hasMany → campaignUser, staff elegível) com
  hook beforeValidate que descarta ids não-staff (fail-closed)
- canRead/UpdateCampaignDemand por membership: assessor só vê demandas onde
  é responsável; unrestricted/admin sempre; leader nunca (fim do escopo
  municipal de demandas)
- criador entra automaticamente como responsável no hook de criação (form,
  wizard, rascunhos C90); migration com backfill created_by_id → responsável
- UI: picker multi-select na criação (nova + wizard) e card de gestão no
  detalhe com save full-replace + advisory lock; sugestões = assessores do
  município; rótulo 'Visível para: Responsáveis · Candidato · Coordenador'
- int tests reescritos para a regra nova + 5 casos novos; e2e novo de
  visibilidade (lista/URL/gestão); fixture e2e com createCampaignDemand
- impl plan + changelog; importMap regenerado com S3_* dummy (OPS69)
2026-08-19 23:55:07 -03:00
Francisco Solla
eebe81cd1e feat(S8): seção 'Nossa história' na home — vídeo da trajetória embutido do YouTube (Closes #91) 2026-08-20 02:19:58 +00:00
Francisco Solla
398a91e54a feat(S9): captura de novidades da campanha na home pública (form + CTA do hero)
Seção 'Receba as novidades da campanha' acima do rodapé: nome + WhatsApp
obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado
'Quero fazer parte do time' gravado como subscription.campaignLevel
(time|esporadico). CTA secundário do hero vira o atalho (#novidades, scroll
suave). Contact + Subscription em withPayloadTransaction com consentimento
novo fail-closed por chave estável 'campanha-novidades' — nada é gravado sem o
texto aprovado no admin. Migration 20260819_192605 (state de contact nullable
+ coluna campaign_level). Impl plan, changelog, int + e2e.
2026-08-20 02:19:58 +00:00
Francisco Solla
cd3f3e2a06 test(S7): pins e2e endurecidos — contrato de borda completo e capa de borda a borda medida 2026-08-19 19:39:09 +00:00
Francisco Solla
4a3c3b709c feat(S7): cards 'Acompanhe de perto' sem raio, borda, sombra ou chip; capa de borda a borda (Closes #90) 2026-08-19 19:39:09 +00:00
Francisco Solla
8bc8900e9d feat(S4): compartilhar conteúdos da home no WhatsApp 2026-08-19 12:50:19 -03:00
Francisco Solla
46038d5a6a fix(e2e S3): goto da home com URL cache-busting no describe da seção de conteúdos
O poll (request.get) convergia no HTML fresco, mas o page.goto servia a
cópia stale do cache heurístico do Chromium (ISR s-maxage sem max-age)
quando a regeneração pós-bust ainda não tinha completado — flake medido no
CI (YT snapshot da S2, 2 flaky) e no prod-mode local (IG snapshot 3/3
retries). cache:'no-cache' não existe nos tipos do Playwright 1.58 e reload
reduz mas não elimina a janela. URL única (?e2e=<ts>) erra todas as
entradas de cache (HTTP + router do Next) e a página sempre vem fresca do
servidor; prod-mode local 9/9 × 2.
2026-08-18 20:30:10 -03:00
Francisco Solla
a638a8acae feat(S3): seção de conteúdos da home de campanha inclui os posts do Instagram
Board "Acompanhe de perto" (S1+S2) ganha o feed automático do perfil oficial:
loader irmão src/utilities/socialFeed/instagramFeed.ts (Graph API /media com
children p/ carrossel + username, unstable_cache tag social-feed + revalidate
300, snapshot cru pré-exclusão persistido via SQL direto, refresh de token
automático com retry 1x — refresh endpoint só renova tokens de Instagram
Login, page tokens caem no fail-closed), global SocialFeedSettings estendida
(instagramEnabled/AccessToken/UserId/MaxItems + snapshot hidden; excludedItems
já cobria a plataforma desde a S2). Requisito nº 1 — picker de exclusão no
admin (primeiro componente custom do repo, campo ui + importmap): lista os
posts do snapshot com thumbnail e toggle "Não exibir" que escreve em
excludedItems. Card com coverAspect square (1:1 do rascunho), badge
Instagram, título = legenda com fallback para post sem legenda, clique no
permalink (noopener), link "Seguir no Instagram →" derivado do username.
Os feeds externos mudam para o subfolder socialFeed/ (pin da S2 previa).
Migration 20260818_192948_add_instagram_social_feed_fields (não destrutiva).
Stub IG no webServer (porta dev+2000, helper socialStub.ts) + 3 e2e novos no
describe serial + kill switch estendido + smoke do picker no admin.
Closes #20
2026-08-18 20:29:50 -03:00
Francisco Solla
cacb6165ad feat(S6): home de campanha sem carrossel no desktop — grade estática em ≥1024px, mobile preservado 2026-08-18 19:32:50 +00:00
Francisco Solla
fe5b11cf24 feat(S2): seção de conteúdos da home de campanha inclui os vídeos do YouTube
Board "Acompanhe de perto" (S1) ganha o feed automático do canal oficial:
global SocialFeedSettings (admin-only, kill switch + toggle + maxItems +
exclusões por item), loader src/utilities/youtubeFeed.ts (Data API v3
search→statistics, unstable_cache tag social-feed + revalidate 300,
fail-closed com snapshot persistido via SQL direto — revalidateTag dentro
de unstable_cache lança no Next 15.4), card generalizado
(CampaignArticleCard → CampaignContentCard, external com noopener) e merge
por recência com o grande = mais recente elegível. Stub HTTP do YouTube no
webServer do Playwright (porta dev+1000) + 4 e2e novos no describe serial.
Migration 20260818_111748_add_social_feed_settings (não destrutiva).
Closes #18
2026-08-18 15:23:50 -03:00
Francisco Solla
a60b3880ca feat(S5): banda visual própria para a seção 'Acompanhe de perto' na home 2026-08-18 13:35:53 -03:00
Francisco Solla
beea467816 fix(S1): revert revalidate=300 — trava o bust por tag no Next 15.4 prod
Medido em probe: com export const revalidate, revalidateTag('posts') para
de regenerar a home (a rota /artigos, sem o export, convergia; a home
ficava stale para sempre). Remove o export e resolve o congelamento da
label relativa por threshold: relativa < 48h, data absoluta além (nunca
mente congelada). Spec: polling positivo do HTML da home pós-revalidate
(ISR serve stale uma vez sob carga) e about:blank antes do cleanup
(prefetch prod de links apagados = 404 no guard).
2026-08-18 05:07:26 +00:00
Francisco Solla
8e18e27c20 fix(e2e S1): poll positivo do HTML da home até convergir pós-revalidate
O warm-up único ainda deixava uma janela de stale-serve sob carga (a
regeneração ISR não completava entre o GET e o goto, e o DOM carregado
nunca refrescava — flake no cleanup sob a suíte paralela). Polling do
estado esperado no HTML do servidor (12x1s) antes do goto torna o
empty-state, o full-state e o cleanup determinísticos.
2026-08-18 05:07:26 +00:00
Francisco Solla
5c48329011 fix(e2e S1): warm-up na home após revalidateTag (ISR pode servir stale uma vez)
Sob a suíte completa em paralelo, a regeneração pós-revalidate às vezes
servia a página velha uma vez; o DOM carregado nunca refrescava e o assert
de cleanup falhava (1 flaky no gate). Requisição GET de warm-up entre o
revalidate e o goto torna o estado determinístico.
2026-08-18 05:07:26 +00:00
Francisco Solla
41dbb564c6 feat(S1): seção de conteúdos na home de campanha (artigos)
Bento 1+4 no desktop e carrossel 1/tela no mobile (~390px) lendo o post
collection com isPostVisible fail-closed; seção some inteira sem posts
visíveis. Zero migration/Consent; home vira ISR com tag posts +
revalidate=300 (label relativa não congela).
2026-08-18 05:07:26 +00:00
Francisco Solla
0aec0d5040 feat(PUB5): slogan 'Mais saúde mais futuro' no hero e rodapé 2026-08-18 01:31:28 +00:00
Francisco Solla
eb281ab5aa fix(e2e C104): clica no slot 14:00 da coluna de hoje, não no centro da linha de hora
O data-time do FullCalendar é uma linha única que atravessa a semana; o
clique force no centro dela resolve o dia pela coordenada x -> coluna do
meio da semana (quinta). Com o término calculado como hoje+1 (C134), o
teste falhava seg/ter quando o término caía antes do início (alert de
validação no overlay). Passa a clicar na coluna do dia atual.
2026-08-17 02:53:02 -03:00
Francisco Solla
665157a0c0 feat(site): hero da campanha responsivo aos boards do Penpot
A primeira dobra da home pública agora acompanha as referências do Penpot
em 393, 768, 1024, 1280, 1366 e 1920 px. No desktop, a escala e a posição
horizontal das fotos dependem somente da largura: Solla, Lula e Wagner
ficam ancorados ao rodapé real do hero mesmo quando o conteúdo aumenta a
seção, e um palco centralizado de 1920 px preserva a proximidade entre
retrato e texto em monitores ultrawide. E2E fixa a geometria das seis
referências, transições de proporção, desktops baixos, ultrawide e os
overrides da WCAG 1.4.12; o caso de regressão também roda no Firefox.
2026-08-16 20:33:00 -03:00
Francisco Solla
b6be493cdf test(site): assenta navegação do cenário mobile 2026-08-16 00:51:38 -03:00
Francisco Solla
7ddc281f22 feat(site): publica home da campanha Jorge Solla 1313 2026-08-16 00:49:24 -03:00
Francisco Solla
d6df8c6360
Merge pull request #741 from fsolla/C139-contatos-pagina-da-entidade-contact-lista-desktop-cards
C139 — Contatos — página da entidade Contact (lista desktop + cards mobile)
2026-08-13 19:27:17 +00:00
Francisco Solla
89867fd884 fix(C139): specs do leader home apontam para meus-contatos (decisão A)
O C139 moveu o leader tool de /campanha/contatos para /campanha/meus-contatos
(LEADER_CONTACTS_HOME); campaignConcepts e campaignHomeActions ainda
assertavam o redirect/link antigos — o gate e2e full pegou os dois.
2026-08-13 16:04:36 -03:00
Francisco Solla
438ac727fe fix(C139): e2e campaignContacts não vaza contatos criados pela UI
O nome letters-only não carrega o runID, então o discoverOwnedRows do
ownership não encontra o contato criado via formulário (create row/sheet)
e a limpeza da fixture o deixava para trás — cada run vazava 2 contatos
no banco de teste. Deletar explicitamente por nome exato no fim do teste.
2026-08-13 15:51:15 -03:00
Francisco Solla
df02e3371a style(C139): prettier no spec campaignContacts 2026-08-13 15:17:49 -03:00
Francisco Solla
a06f810a40
Merge pull request #739 from fsolla/C134-e2e-agenda-dia-hardcoded-17-de-agosto-de-2026-no-teste-
test(C134): e2e agenda deriva o dia do picker de término de hoje + 1 (sem data hardcoded)
2026-08-13 18:17:27 +00:00
Francisco Solla
0b4d7913fe test(C139): suite e2e campaignContacts e remapeamento do manifest
Cobre a página /campanha/contatos: recorte por omnibox + WhatsApp na
linha, create row desktop (erro de validação mantém o valor digitado,
conflito de nome com mensagem segura, create válido com POST), gate de
staff (líder cai em meus-contatos) e os sheets mobile (create via FAB
com telefone, edit com conflito + ficha atômica). Nomes criados via UI
são letras-only: o runID hex vira [a-p] (bijection, 128 bits).

O manifest ganha a família própria campaignContacts (antes caía no
campaignHomeActions).
2026-08-13 15:16:34 -03:00
Francisco Solla
db8ee29229 test(C134): asserção do término como string exata (convenção: sem regex de template não ancorado) 2026-08-13 15:06:41 -03:00
Francisco Solla
fe0a7999a9 test(C134): e2e agenda deriva o dia do picker de término de hoje + 1 (sem data hardcoded)
O teste C104 clicava em '17 de agosto de 2026' fixo no picker de término;
quebrava deterministicamente ~2026-09, quando o dia sairia do grid visível.
Agora o alvo é civilDatePlusDays(formatBahiaCivilDate(new Date()), 1) —
sempre dentro do grid (dia do mês ou day 1 do mês seguinte como outside day).
Absorve também o flake pré-existente do redirect canônico ?tab=overview nos
3 sítios de toHaveURL do detalhe (mesmo regex da linha 219, C123).
2026-08-13 15:01:23 -03:00
Francisco Solla
312c0048bd fix(OPS45): extrair purge de resíduo para módulo vitest-free (e2e quebrava no CI)
O import do campaignFixtures.ts (que importa vitest) no fixture e2e crashava a
coleção no processo do Playwright: TypeError: Cannot redefine property:
Symbol($jest-matchers-object) em @vitest/expect — falha nos 2 shards e2e do CI.
purgeMunicipalityResidue/relationId/relationIds/RelationshipValue agora vivem em
tests/helpers/campaignResidue.ts (sem vitest), re-exportados por campaignFixtures
para os importadores existentes.
2026-08-13 14:40:18 -03:00
Francisco Solla
bf25100ed5 chore(OPS45): e2e fixtures — purge de resíduo no claim de município + robustez dos int specs (Closes #723)
O cleanup do campaignE2EFixtures estava completo em runs normais (proxy auto-own
cobre contact/stateDeputy); o lixo vinha de runs crashados, cujo cleanup nunca
roda. O claim e2e agora purga resíduo como o int já fazia, e a purga ganhou a
classe dobradinha (stateDeputy vinculados via municipality.stateDeputies + fichas
órfãs com ref-check incluindo stateDeputy — FK ON DELETE RESTRICT).

peopleList.int.spec.ts passa a varrer todas as páginas (lista global de 25/pág)
com dedupe; personDelete/peopleList usam personName() nos nomes fixos, imunes ao
invariante global de nome de dobradinha.
2026-08-13 14:18:54 -03:00
Francisco Solla
1daaaf9f4e feat(C139): domínio de contatos — URL state, loader com vínculos e invariante de nome
Fase 2 do plano aprovado. Três módulos em src/utilities/contacts/:
- contactListUrl: contrato de params (q|gender|state|city|ausencia|vinculo|sort|dir|page),
  canonicalize, hrefs de sort e where de fonte com 'phones.value' like (ILIKE verificado
  no Postgres local).
- contactListData: loader com UM find em contact (user threadado, overrideAccess false),
  vínculos (liderancas/dobradinhas/assessores/equipe) como facets sobre o conjunto
  legível, filtro/sort/paginação em memória, nulos no fim (precedente B15).
- contactNameInvariant: lock contact-name:{normalized} + prefilter ILIKE + veredito
  exato em memória; mensagem de conflito pt-BR.

38 testes unit novos; format + tsc limpos.
2026-08-13 14:05:24 -03:00
Francisco Solla
98b03d8e91
Merge pull request #718 from fsolla/C130-pessoas-ajustes-da-tabela-desktop-contato-completo-base
C130 — Pessoas: ajustes da tabela desktop (contato completo, base sob o nome, caret, tooltips, Dobra em, filtro/ordenação por partido)
2026-08-12 16:20:44 -03:00
Francisco Solla
ef41e0c6a3
Merge pull request #726 from fsolla/B200-municipios-mobile-encaixes-dos-cards-densidade-alinhame
B200 — Municípios mobile: encaixes dos cards — densidade, alinhamento de labels e avatares (pós-B196)
2026-08-12 16:01:40 -03:00
Francisco Solla
65659bac2f C130: ajustes da tabela desktop de pessoas (contato completo, base sob o nome, caret, tooltips, Dobra em, filtro/ordenação por partido) 2026-08-12 16:01:25 -03:00
Francisco Solla
98f8ec9b9b
Merge pull request #710 from fsolla/C128-pessoas-qualquer-pessoa-pode-assessorar-liderar-e-ou-do
C128 — Pessoas: qualquer pessoa pode assessorar, liderar e/ou dobrar — ciclo de vida nas tabelas pela lista
2026-08-12 15:45:55 -03:00
Francisco Solla
3ee2d83350 feat(B200): encaixes dos cards mobile de municípios — densidade, alinhamento de labels e pilha de avatares (pós-B196) 2026-08-12 15:40:02 -03:00
Francisco Solla
ca188bb03a fix(C128): e2e do C131 escopa a busca à célula Lidera — células de capacidade vazias agora são editáveis 2026-08-12 15:29:56 -03:00
Francisco Solla
c0645067f1 feat(C128): ciclo de vida das capacidades pelas células de /campanha/pessoas (Closes #695)
Qualquer pessoa visível ganha/perde Assessora, Lidera e Aliada em pela
lista: a primeira cidade cria a entidade (conta staff sem credenciais
utilizáveis / liderança / dobradinha), a última remove da tabela com
confirmação destrutiva (manifest: votos+convites da liderança, conta+
autorias+assessorado da assessora; dobradinha sem diálogo). Saída
destrutiva por escopo: assessor encerra Lidera/Aliada com a capacidade
inteira na carteira; Assessora segue coordenação/candidato. Escritas
person-centric por contactId com extração dos deltas B34/B37 e da
cascata de conta (deleteCampaignUserAccount, nextFreeStubCampaignEmail,
matchers de conflito) — sem twin. commitGuard/commitWithNullOwner/
extraFormFields como opt-ins no RelationChipCell. Corrige bug latente
C116 (célula Assessora nunca salvava — contactId faltava no FormData).
Sem migration. Int: 28, unit: 6, e2e: +2.
2026-08-12 15:29:56 -03:00
Francisco Solla
99a3a5f590 test(OPS42): e2e dev — quiescência do router antes de interagir (login + chat Sollinha) (Closes #691) 2026-08-12 15:03:20 -03:00
Francisco Solla
db8102b47e feat(C123): agenda — overlay modal central como única superfície de criação/edição de atividade (Closes #667) 2026-08-12 14:16:20 -03:00
Francisco Solla
7e05dc925d feat(C131): Salvador como opção agregada no dropdown de municípios das células de capacidade
O dropdown de adição de municípios (pessoas ×3, assessores, lideranças,
dobradinhas) oferece 'Salvador — Todas as zonas' como primeiro hit da busca:
um toque adiciona as 19 ZEs num lote, via máquina existente do RelationChipCell
(delta otimista, cap/floor, undo, colapso 'Salvador (19)' B178, exact-match C116).

- searchMunicipalityPortfolio ganha kind 'city' (NORMALIZED_ZONE_CITIES,
  derivado do catálogo) com gate de escopo por contagem: o agregado só aparece
  quando a cidade inteira está no escopo addableIds do ator — a label 'Todas as
  zonas' nunca mente para assessor com carteira parcial.
- hitDescription ganha o branch 'Todas as zonas'; sem migration/access/schema.
- Testes: unit busca pura (19 ids, soma, escopo parcial, tudo atribuído,
  posição), cell render, e2e em campaignPeople (agregado → chip → 19 zonas no
  DB).
2026-08-12 14:02:34 -03:00
Francisco Solla
39e6e9405c style(OPS30): prettier — formatação do login retry no fixture 2026-08-12 13:34:42 -03:00
Francisco Solla
48088d8162 refactor(OPS30): fixes do /simplify — pessoas no prewarm, login com sub-budgets, retry da demanda via existência, B24 no helper do arquivo
Review 3× read-only (0 P1):
- setup: /campanha/pessoas faltava (campaignPeople); log do 1º hit
  quando o retry salva; comentário do budget ancorado no medido.
- login: sub-budgets 15+5+30 (antes 30+5+30 estourava o test timeout
  e credencial errada morria de timeout genérico); retry bifurca pela
  URL real (dashboard/fora do login/re-submit) — re-submit cego
  contra página que saiu do login falhava com fill na dashboard.
- :713: retry cego criaria duplicata que o UNIQUE do slug refuta —
  o retry consulta por título e navega ao detail quando o 1º POST
  criou mas o redirect se perdeu.
- B24: locator reusa visibleMunicipalityButton (escapeRegExp +
  fronteira de nome, 23/435 com prefixo) e ensureWideMunicipalityList
  re-rodado pós-reload (padrão do irmão dobradinhas).
- :1063: waitForResponse com timeout 5s para o toPass decidir.

Re-prova: campaignMunicipalities 22/22 (arquivo atual, B193+OPS29 do
main) ×3 consecutivos em dev a 2 workers (load 5-17).
2026-08-12 13:34:42 -03:00