Commit graph

789 commits

Author SHA1 Message Date
Francisco Solla
02c1e4f36b feat(OPS71): Fase 2 — remover arquivos do fluxo Forgejo (workflows/scripts/specs) pós-validação do GitHub Actions
14 arquivos removidos: .forgejo/workflows/* (6), forgejo-pr-automerge.mjs,
forgejo-dispatch.mjs, ci-classify-production.mjs, lib/dockerignore.mjs,
lib/branch-protection.mjs + 3 specs órfãs. Consumidores ajustados:
test-affected-core (HIGH_RISK_EXACT), ciSkipInvariants (re-ancorado no
deploy.yml dispatch-only — deploy nunca automático), comentários com
referência morta (gate-ci, github-pr-automerge, agent-pr-ready-automerge).
Docs: AGENTS.md, runbook (rollback via git history), README (era GitHub).
Tracker intocado (forgejo-api/agent-*/issue-transition ficam).
2026-08-20 00:20:10 +00:00
Francisco Solla
f5862c9ba3 fix(OPS71): ciClassifyProduction hermético — env GIT_* limpa + assert do scratch repo (não polui o worktree externo) 2026-08-19 23:07:34 +00:00
Francisco Solla
0901d60d72 fix(OPS71): literal do required check = nome do check-run (checks) + docs (pin PR #742) 2026-08-19 23:07:34 +00:00
Francisco Solla
8820a4427f fix(OPS71): pins do forgejoApi determinísticos sob GITHUB_SERVER_URL do GitHub Actions 2026-08-19 19:17:19 -03:00
Francisco Solla
24a867dfc3 feat(OPS71): CI/PR/deploy no GitHub Actions — tracker de Issues permanece no Forgejo (Closes #97) 2026-08-19 19:17:19 -03: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
fbfbfb1329 Merge pull request 'S4 — Compartilhar conteúdos da home no WhatsApp' (#86) from S4-compartilhar-conteudos-da-home-no-whatsapp into main 2026-08-19 16:26:43 +00:00
Francisco Solla
39fb3b47f0 fix(OPS62-followup): âncora de relógio no fixture do municipalitiesWithoutUpdateTool — elimina o flake de ordenação por raiz
Causa raiz (reproduzida, refuta a hipótese da Issue): daysAgo() chamava
Date.now() por chamada; as duas daysAgo(40) de Beta/Alpha podiam cair em ms
diferentes sob carga, deixando Alpha estritamente mais novo — sortStale
ordenava por timestamp e o tie-break por nome nunca disparava (0.08% das
construções sem pausa; 100% com pausa de 1 ms; fixture ancorado: 0 em 2000).
Fix test-only: âncora fixtureNowMs no load do módulo (padrão prioritiesTool)
+ hoist do timestamp empatado (igualdade estrutural) + comentário.
Nada muda em produção — o sort real é determinístico sobre timestamps fixos.
Verificação: spec 30/30, unit 2405, int 791, tsc/lint/format/cycles verdes.
2026-08-19 13:07:30 -03:00
Francisco Solla
8bc8900e9d feat(S4): compartilhar conteúdos da home no WhatsApp 2026-08-19 12:50:19 -03:00
Francisco Solla
ccfa60c1f2 fix(OPS64): pin do waitForChecks contra rollup mentiroso + docs — OPS62 eliminou o secundário estruturalmente
O sintoma primário da Issue (deadlock do waitForChecks no próprio pending)
já tinha sido entregue pelo OPS61. O secundário — o rollup `checks` reportava
SUCCESS com um shard de e2e vermelho porque o Forgejo 9.0.3 reporta o
resultado do ÚLTIMO shard como o parent de matrix no `needs` (runs 604/607/610;
o `needs.<job>.results` do GitHub não existe neste runner) — foi eliminado
estruturalmente pelo OPS62 (mergeado durante a execução): ci-pr.yml e ci.yml
viraram jobs únicos sequenciais, sem matrix/shard, e o status do job `checks`
já é o veredito honesto da cascata.

O que este item entrega: (a) teste-pino no waitForChecks (cenário run 607:
rollup verde + shard vermelho → o CLI LANÇA "Checks falharam" — nunca confia
no rollup sozinho; invariante de defesa válido e que pegaria qualquer matrix
que volte); (b) docs honestas (agent-pr-workflow.mdc, AGENT-OPS.md) com a
narrativa do parent de matrix e da eliminação estrutural pelo OPS62. O script
ci-rollup-check.mjs e os edits de workflow planejados foram descartados no
gate (redundantes pós-OPS62). Unit 2392 verdes; gates locais ok.
2026-08-19 12:11:41 -03:00
Francisco Solla
cc6149191b ops(OPS67): retry com backoff nos fetchs do forgejo-api
Um fetch failed transitorio (rede/DNS/reset) matava o run do safety net
(agent-pr-ready-automerge) no meio da espera de checks. Retry no request()
do cliente: fetch reject retenta em qualquer metodo; body-drop em GET
retenta; 5xx (502/503/504) retenta so em GET (body cancelado antes do
retry); 4xx e body-drop em escrita falham fail-closed. 3 retries, base
300ms x2, jitter +-20%, console.warn pt-BR por retry; opcoes flat no
createApi (retries/backoffMs/jitter/sleepImpl). 12 specs unit novas.
2026-08-19 13:28:37 +00:00
Francisco Solla
8d33e89102 style: prettier — runbook/impl plan/unit test (OPS66) 2026-08-19 10:07:14 -03:00
Francisco Solla
228f2217c7 ops(OPS66): deploy — migrate antes do build do runner (deadlock de migrations em caminho estático) 2026-08-19 10:06:00 -03:00
Francisco Solla
9504c7ccf6 ops(OPS65): CI do main em janela de 30 min (gate .dockerignore fail-open) + matar agent-pool
- ci.yml: on push removido → schedule */30 + workflow_dispatch (fundido com a
  estrutura de job único sequencial do OPS62 X1)
- job gate (host, main-only, sem pnpm): revision do container rodando no
  homeserver (label org.opencontainers.image.revision — o compose pode mentir
  após rollback falho) + diff + classificador .dockerignore
- fail-open: revision desconhecida → suite roda (verde); incapaz de classificar
  → run vermelho (nunca blackout silencioso); deploy duplo-gated
  (needs: [checks, gate] + if explícito)
- deploy-homeserver.sh: guard 'already deployed' idempotente (mesma revision)
- agent-pool.yml removido; pool dormente (144 pnpm install/dia no tick */10)
- specs: dockerignoreMatch, ciClassifyProduction (incl. git-diff com repo
  scratch e exit codes), pins ciSkipInvariants/deployScript; HIGH_RISK_EXACT
- docs: runbook (janela/gate/verificação ao vivo/falhas), AGENTS.md,
  AGENT-OPS.md (tabela X1 fundida), skills pool dormente, changelog OPS65
- impl plan aprovado; intenção marcada entregue
2026-08-18 21:08:12 -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
ee31aeee4f fix(OPS62): services por nome na rede do job — Forgejo 9 não expande expressões em ports
Achado ao vivo (run 730): a expressão ${{ 5433 + (github.run_id % 100) }} no
ports do service não é avaliada pelo Forgejo 9 — o literal ia ao runner e o
job falhava em 1s. E portas fixas colidiriam entre runs concorrentes (o job
único segura os services pelo run inteiro). Correção: services sem publish
host, alcançados por nome na rede do job (postgres-int/postgres-build:5432)
— solução canônica do Actions, mata a colisão de raiz. Guards fail-closed
(LOCAL_HOSTS + assertTestDatabase) admitem os nomes dos services (hosts
internos do runner; hosts remotos continuam rejeitados); pins novos em
localHosts.unit.spec.ts e ciSkipInvariants (sem ports:, URLs por nome).
2026-08-18 23:26:13 +00:00
Francisco Solla
12a4fe1005 chore(OPS62): CI fail-fast estrutural — job único checks sequencial (X1)
ci-pr.yml e ci.yml deixam de ter jobs irmãos: 1 job 'checks' com steps em
série (guards → lint → format → typecheck → knip → cycles → unit → int →
build → e2e). Step vermelho interrompe o job, o run falha e o slot é
liberado — sem API de cancel (Forgejo 9.0.3 não tem), sem watchdog, sem
upgrade do Forgejo. Skips do ci-scope.mjs viram step id 'scope'; e2e em 1
processo com PLAYWRIGHT_WORKERS=4 (substitui 2 shards × 2 workers); 2
services Postgres por run com portas host derivadas do run_id (runs
concorrentes não colidem); contexto 'CI (PR) / checks' preservado (job id
checks — contrato do automerger OPS61); deploy OPS53 gated em needs checks.
Plumbing morto removido (e2eShardConfig/e2e_shards). Docs: AGENT-OPS,
TESTING.md, changelog. Impl: docs/plans/ops62-ci-fail-fast-forgejo-impl.md
(a intenção watchdog+upgrade fica como histórico do gate).
2026-08-18 23:26:13 +00:00
Francisco Solla
8c915e59fc fix(S6-FOLLOWUP): test:e2e:affected rodava a suíte completa — --no-deps desliga o closure de deps dos projects em runs filtrados
Causa raiz (medida com o binário 1.58.2, config real): commander parseia
posicionais e -g/--list funcionam; o que arrastava a suíte era o closure de
dependências do dev mode (setup<-campaign<-frontend<-admin): filtrar um spec
do frontend puxava campaign(160)+setup(2). run-e2e-affected.mjs agora gera
test:e2e <passthrough verbatim> --no-deps -- <paths> via helpers puros em
scripts/lib/playwright-e2e-args.mjs (unit-pinned); passthrough corrigido
(pnpm consome o -- e o path era descartado); run-e2e-affected.mjs entrou em
HIGH_RISK_EXACT. Docs: receitas de filtro no playwright.config.ts, AGENTS.md,
changelog.
2026-08-18 19:09:12 -03:00
Francisco Solla
8b98930670 fix(OPS61): gate do waitForChecks sem o pending do próprio job — achado ao vivo PR #67
O Forgejo posta o status do próprio job do safety net como pending durante o
run inteiro: o gate antigo (pending.length === 0) era insatisfazível e o CLI
nunca auto-mergeou nada — todo merge Forgejo até aqui foi manual (merged_by
do dono), inclusive a PR #52 com CI vermelho (narrativa da 'raça' corrigida).
O gate é: rollup CI (PR) / checks presente+verde (cascata inteira assentou),
ignorando o contexto do próprio workflow; mergeable false só após assentar.
2026-08-18 17:45:54 -03:00
Francisco Solla
ac11afecf3 ops(OPS61): flip de issues pós-merge com PAT + gate do rollup checks + branch protection real no Forgejo
- issue-done/plan-issue-ready: env FORGEJO_API_TOKEN (GITHUB_TOKEN 403 em
  Issues) + fork guard (checkout de fork roda script do fork com o PAT — RCE)
- forgejo-issue-transition: falha de flip vira exit 1 (job vermelho, nunca
  skip silencioso)
- waitForChecks: exige o rollup CI (PR) / checks presente+verde (raça PR #52:
  snapshot com jobs não-agendados parecia verde); mergeable=false só conta
  como conflito após a cascata assentar; timeouts 30→45min (CLI) e 40→55min
  (workflow)
- configure-branch-protection: contexto real CI (PR) / checks* (Forgejo casa
  por glob contra 'CI (PR) / checks (pull_request)'; literal 'checks' nunca
  casaria), idempotente GET→POST/PATCH, verificação pós-aplicação; decisão
  pura em scripts/lib/branch-protection.mjs
- docs honestas (AGENT-OPS, agent-pr-workflow.mdc, CUTOVER-MAIN-ONLY, README)
- testes: raça do rollup/rollup failure/mergeable pré-settle +
  branchProtection create/update/noop + glob real
2026-08-18 17:45:54 -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
7bd9c9504f ops(OPS59): pre-push sem e2e — e2e roda uma vez, no CI (gate local fica com os outros testes) 2026-08-18 17:56:41 +00:00
Francisco Solla
228d5da9c8 ops(OPS52-media-guard): reconcile do media:recover exige MEDIA_RECOVER_CONFIRM=1 (guard de intenção explícita) 2026-08-18 12:45:32 -03:00
Francisco Solla
7b8e37e162 fix(ops OPS57): simplify — veto de draft vale em conversão mid-run; CLI valida base main; docs sem mecanismo fantasma 2026-08-18 15:24:21 +00:00
Francisco Solla
b78a390371 fix(ops OPS56): forgejo-pr-automerge — veredito de merge por re-read do PR (falso positivo 'já mergeado' com PR aberto) 2026-08-18 14:28:54 +00:00
Francisco Solla
45e29a7785 feat(OPS52-media): recupera os objetos de media de produção do WordPress para o bucket (pnpm media:recover)
- scripts/recover-media.mjs: CLI de reconciliação (reconcile/--dry-run/--verify)
  — lê rows de media + posts.coverImage, resolve a capa com a mesma lógica do
  seed (featured media, senão 1ª imagem inline), PutObject com key = filename;
  DB nunca escrito; fail-closed (S3_* completas, ALLOW_REMOTE_DB, echo do alvo,
  timeouts curtos, covers só do origin WP e image/*)
- scripts/lib/wpArticles.mjs: fetch ao vivo do WordPress extraído do seed
  (sem twin), helpers puros de resolução + row→cover mapping (unit-testados)
- seed-posts.mjs: importa o fetch da lib (comportamento idêntico) e usa
  resolveCoverSource em processContent
- runbook de execução no homeserver (impl plan), nota no AGENTS.md, changelog
Closes #10
2026-08-18 08:20:45 -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
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
54058da059 OPS52 — Migrar a media de produção do Vercel Blob para o Garage S3
Merge pull request #27 from fsolla/OPS52-migrar-a-media-de-producao-do-vercel-blob-para-o-garag

OPS52 — Migrar a media de produção do Vercel Blob para o Garage S3 (Closes #3)
2026-08-18 02:45:52 +00:00
Francisco Solla
a34cd9b68a fix(ops OPS53): deploy — compose run consome o stdin do bash -s (migrate com </dev/null); build via --network host + proxy socat (BuildKit recusa rede bridge custom) 2026-08-18 01:59:17 +00:00
Francisco Solla
dabc00f46c ops(OPS52): migra a media de produção do Vercel Blob para o Garage S3
- payload.config.ts: vercelBlobStorage -> s3Storage (@payloadcms/storage-s3)
  com endpoint Garage (forcePathStyle, region garage); bucket privado — o
  staticHandler do plugin streama os objetos em /api/media/file/<filename>,
  mantendo o contrato de URL relativa das 40 rows pré-existentes
- src/utilities/mediaStorage.ts: resolveS3StorageEnv fail-closed (S3_* parcial
  -> throw no boot com a lista exata; nenhuma -> storage local dev/test)
- seed-posts: remove pre-delete do @vercel/blob (S3 sobrescreve nativamente)
- worktree-env.mjs: s3EnvCopiedLines all-or-nothing (unit-testada)
- envs S3_BUCKET/S3_ENDPOINT/S3_REGION/S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY
  no .env.example/.env.test; cloud-setup.sh sem o sed morto do blob
- deps: -storage-vercel-blob -@vercel/blob +storage-s3; importMap regenerado
- docs: impl plan (revisão A->B: Garage v2 sem acesso anônimo na API S3),
  AGENTS.md, changelog 2026-08-18-ops52
2026-08-17 22:36:34 -03: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
6a3f61f968 ops(OPS53): etapa de deploy no CI para o homeserver (teqo-1313)
Job deploy em ci.yml (runs-on host na workstation) streama
scripts/deploy-homeserver.sh para o homeserver via SSH: build dos stages
runner+migrator com secrets do teqo-1313.env, push no registry local,
migrate pelo serviço de maintenance antes do rollout, up + smoke. Zero
secrets novos no Forgejo; HEAD guard + flock; rollback automático com
backup do compose; runbook docs/ops/teqo-1313-deploy.md; invariantes
unit do script (bash -n + estrutura).
2026-08-18 00:50:41 +00:00
Francisco Solla
f151376641 ops(OPS50): remove deploy Vercel do pipeline — produção é self-hosted no homeserver (jorgesolla1313.com.br); ci.yml vira verificador puro (sem deploy/requeue), apaga vercel-promote.yml, scripts/tests/vercel.json e secrets VERCEL_* 2026-08-17 13:17:17 -03:00
Francisco Solla
ac664093d2 fix(ci OPS50): build e2e em .next-e2e (o webServer do playwright serve esse dir desde 7ca0efc9 — os workflows ainda buildavam em .next/e2e e o 'next start' não achava o bundle) + invariante unit passa a pinar os workflows 2026-08-17 11:33:01 -03:00
Francisco Solla
02f68894c3 fix(ci OPS50): guards de banco aceitam o gateway da bridge do runner (RFC1918) — postgres do services: fica no IP do gateway, não em localhost 2026-08-17 03:46:16 -03:00
Francisco Solla
f20305759a OPS50: port CI e scripts de agentes do GitHub para o Forgejo
Workflows nativos em .forgejo/workflows/ (ci, ci-pr, agent-pool,
automerge, issue-transition, plan-issue, vercel-promote, archive),
removendo .github/workflows e .github/actions. Runner Forgejo v13 com
services Postgres por porta + DB_HOST via gateway do bridge. Libs de
agente sobre o novo cliente forgejo-api.mjs (zero dependências,
GITHUB_SERVER_URL/FORGEJO_API_URL), helpers de workflow plain-Node.
pnpm issue corrigido (await em função sync) e validado ao vivo. Testes:
forgejoApi.unit.spec.ts novo, pin do matrix do ci.yml atualizado.

Closes #1 (parte 1 — docs/issues/pool seguem na parte 2)
2026-08-16 23:32:20 -03:00
Francisco Solla
e0d2372d3b fix(ci): ignora artefatos do build e2e 2026-08-16 01:35:49 -03:00
Francisco Solla
7ca0efc95c fix(ci): isola bundle e2e do servidor de desenvolvimento 2026-08-16 01:25:06 -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
9471c8f0aa OPS44: changelog por entrega + guards de docs + merge por rebase 2026-08-13 17:55:22 +00:00
Francisco Solla
621364f41f feat(C139): página /campanha/contatos — lista desktop com células editáveis, criação e filtros 2026-08-13 14:29:11 -03:00
Francisco Solla
e71b062650 feat(C139): schemas e actions de contato — criação staff e edição por célula
Fase 3 (server) do plano aprovado:
- schemas/contact.ts: contactCreateSchema (nome único via invariante, state
  obrigatório, phones opcionais C112); contactFieldUpdateSchema ganha variantes
  city e postalCode ('' → clear); mensagens das actions client-safe aqui.
- schemas/personGenders.ts: enum de gênero em módulo folha (ciclo de imports
  contact ↔ leadership detectado e quebrado).
- actions/contact.ts: createContact (gate staff + bypass justificado, lock
  contact-name:{normalized}) e updateContactField (escopo 'edita o que vê'
  via re-check user-threaded para advisor; reorder de telefone primário).
  Apagar reusa as actions C100 existentes.
- collections Contact e Supporter: label de gênero 'Outro' → 'Não binário'
  (sem migration; nenhum teste pina a label).
- 11 testes unit novos; lint, tsc e prettier limpos.
2026-08-13 14:12:53 -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
9e8b635f32 C130: rebase em main — C125 (sort primário na omnibox) e ausência 'qualquer' reconciliados (party + Dobra em nos testes) 2026-08-12 16:01:25 -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
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
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
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
3593d84b30 refactor(C129): simplify — CampaignNameSubline compartilhado (a11y sr-label), guard legenda==nome, sem plumb morto, spec mais forte 2026-08-12 12:52:35 -03:00
Francisco Solla
d7acb5aa48 feat(C129): nome de legenda da dobradinha — campo ballotName + display discreto sob o nome (Closes #696) 2026-08-12 12:52:35 -03:00
Francisco Solla
2f247d40a6 fix(C115): revisão /simplify — relógio com baseline de espelho, reopen sem re-cancelamento, hardening do webhook
- lastMirroredChangeAt (activity, migration): clock rule compara contra a
  última mudança espelhada, não updatedAt (task toggle não silencia edição
  pendente do Google)
- snapshot exclui ids que o motor deletou (removedEventIds separado do
  remoteEventIds): reopen pós-cancelamento não vira re-cancelamento fantasma
- cancelamento do Google respeita o relógio: cancel mais novo cancela; Teqo
  mais novo (reopen) limpa o trashed e recria
- escrita reversa sem req.user: autoria do registro correta + sem gate 403
  do deputado; hook de sync pula a própria escrita reversa (sem passada aninhada)
- canal: segredo rotaciona por criação; novo canal persistido antes do stop
  do antigo; identity fields read:false; create admin-only; leitura prefere
  linha configurada; pre-filter do webhook antes do bootstrap do Payload
- testes: all-day ida-e-volta, task-toggle, reopen, slug congelado, e2e dos
  caminhos 200, render tests do dialog/feed; asserções herméticas a
  atividades ambiente dos arquivos irmãos (DB compartilhado)
2026-08-11 22:42:05 -03:00
Francisco Solla
7629a505e0 feat(C115): edição pelo Google Calendar volta para o Teqo (motor bidirecional + webhook push)
- runSyncPass bidirecional: regra do relógio decide direção (Google mais novo
  aplica título/horário; Teqo re-asserta), cancelamento via showDeleted +
  snapshot lastSeenEventIds, registro no updates feed
- webhook público /campanha/agenda/google-webhook/[secret] fail-closed
  (secret + X-Goog headers constant-time), canal lazy sem cron (ensure em
  toda passada, renovação com id novo + stop do antigo)
- migration hand-written (campos de canal/snapshot em googleCalendarSync);
  mutationKind dedicado libera título via Google sem quebrar slug canônico
- UI: seção 'Edições pelo Google' no dialog + autor 'Google Calendar' no feed
- testes: unit (reverse mapping, validação webhook), int (motor bidirecional
  com stub, 10 cenários), e2e (webhook 404 fail-closed)
2026-08-11 22:38:53 -03:00
Francisco Solla
c08be6c46a
Merge pull request #707 from fsolla/C125-c125-pessoas-pos-c117-ux-descoberta-de-ordenacao-no-mob
C125: Pessoas pós-C117 UX — descoberta de ordenação no mobile e facet de ausência completo
2026-08-12 00:04:50 +00:00
Francisco Solla
6d41517a05
Merge pull request #705 from fsolla/C122-c114-follow-up-cobertura-de-testes-dos-estados-do-espel
C122 — cobertura de testes dos estados do espelho Google (componente + e2e + action)
2026-08-11 23:49:39 +00:00
Francisco Solla
7fabb72df8 feat(C125): facet de ausência completo (qualquer_ausencia) e descoberta de ordenação no mobile
- PEOPLE_ABSENCES ganha o 4º valor qualquer_ausencia ("Qualquer ausência"),
  a união exata das três ausências em UM chip — o 3º clique dos específicos
  deixa de colapsar o facet (3 de 4 membros) e o parse absorve o guarda-chuva
  sobre os específicos, sem tocar no B18 genérico de capacity/status
- peopleListSortPrimaryOptions: seeds do grupo Ordenação viram as 7 direções
  primárias com emptyQueryVisible, então o mobile descobre as 7 chaves ao
  tocar no bar (e "ordenar" não corta aliada/assessorado/base no cap 8)
- CampaignListOmnibox reabre o popover no mousedown: clicar num input já
  focado (logo após commitar busca com Enter) não disparava focus, deixando
  a descoberta morta no mobile até digitar
- tests: contrato de URL (absorção + não-colapso), predicado + AND, novo spec
  de omnibox, e2e do 3º clique e da descoberta mobile (390×844)
2026-08-11 20:34:51 -03:00
Francisco Solla
d6dfdf70ed fix(OPS41): resolve marcadores de conflito commitados no AGENTS.md + guardrail anti-reincidência (#689)
O commit OPS33 (f0f5b73f) entrou com o arquivo já conflitado na região do
bullet 'Per-worktree environments' (<<<<<<< HEAD indentado + lado HEAD
pré-OPS33 com artefato '- # ' + fechamento corrompido '> > > > > > >'; o
======= foi removido no próprio commit) e o OPS37 herdou o estado.

Resolução perda zero validada por diff: manter o lado superset (OPS28 +
OPS31 + OPS33 — claim determinístico, --issue N, prompt citado) e
descartar o lado HEAD; -3 linhas no total; scan de todos os trackeados
confirmou AGENTS.md como único arquivo afetado.

Guardrail: describe 'no conflict markers in committed files' em
codebaseConventions.unit.spec.ts — varre git ls-files e falha em
<<<<<<< / >>>>>>> (tolerante a indentação) e na forma corrompida
^\s*(> ){7}; validado negativo contra o estado sujo (AGENTS.md:37, 40).
2026-08-11 20:23:50 -03:00
Francisco Solla
5940c4c61e fix(OPS40): pnpm dev honra o PORT do .env.local — next dev -p via scripts/dev.mjs (Closes #682)
next dev resolve a porta antes de @next/env carregar env-files (commander
.env('PORT') no bin/next), então o PORT=3100+slot do provisionador de
worktree era ignorado e todo dev server subia em 3000. pnpm dev/devsafe
passam por scripts/dev.mjs: preflight do guard-dev-db inline, env com a
mesma precedência (real > .env.local > .env, override:false) e porta como
flag CLI -p (resolveDevPort/nextDevArgs em scripts/lib/cli.mjs,
unit-testados). Sem PORT → nenhum -p → default 3000 + allowRetry intactos.
2026-08-11 20:04:07 -03:00
Francisco Solla
c7c9556ff3 test(C122): cobertura dos estados do espelho Google — unit do chrome, e2e dos 4 estados + FAB, int da action de desativar 2026-08-11 19:43:19 -03:00
Francisco Solla
b604157fc7
Merge pull request #685 from fsolla/OPS39-prewarm-e2e-dev-adicionar-campanha-home-search-post-e-
OPS39 — Prewarm e2e: home-search POST e mudar-tendencia GET no setup spec
2026-08-11 20:14:51 +00:00
Francisco Solla
21b4b4d0c6 fix(gate): seleção e2e contendo apenas o spec setup vira none (setup é dev-only; OPS39) 2026-08-11 15:47:12 -03:00
Francisco Solla
a8414a49f1 Merge origin/main into C117 (C116/C119/OPS40) — sortable heads sobre células editáveis 2026-08-11 15:43:23 -03:00
Francisco Solla
f1405b04a5
Merge pull request #680 from fsolla/C119-status-de-apoio-lembranca-em-liderancas
C119 — Status de apoio 'Lembrança' em lideranças
2026-08-11 18:25:18 +00:00
Francisco Solla
a83bedd7a8 feat(C116): pessoas — edição onde você vê na lista desktop (células sempre input, chips territoriais internos, nome-link, tooltips nas ações) 2026-08-11 15:09:56 -03:00
Francisco Solla
c91b6a508d test(C119): pina facet de status na lista de pessoas, parse exaustivo da URL de pessoas e variant do badge 2026-08-11 14:52:49 -03:00
Francisco Solla
5fa4e22c9d feat(C119): status de apoio 'Lembrança' em lideranças — enum, badge e exclusão de pendências 2026-08-11 14:52:49 -03:00
Francisco Solla
aa8fe582c0 feat(B198): drawer mobile do Sollinha fecha ao navegar por link interno
O renderer de links do chat decide interno vs externo (B187/B188); a decisão
pura de fechamento mora no owner src/lib/ai/markdownLinks.ts
(shouldCloseDrawerOnLinkClick: interno && mobile && clique simples) e o
markdownComponents estatico virou useMemo no corpo do CampaignAIChat para
alcancar o contexto. Desktop inalterado; links externos/modos de nova aba
mantem o drawer da aba de origem; sessao e restore intactos (close persiste
open:false).

- unit: 12 testes da decisao pura
- e2e (modo CI, E2E_PROD=1): interno fecha + navega sem load + reload nao
  reabre; externo abre popup e mantem o drawer
- debito B199/#672 (depends #660): race de persistencia ao fechar mid-stream
2026-08-11 14:29:29 -03:00
Francisco Solla
70070db8dc feat(C114): agenda da campanha espelhada no Google Calendar com notificação (push Teqo → Google)
Espelho oficial da agenda em calendário Google compartilhado via API
(service account, jose + fetch): reconciliação por passada com id
determinístico teqo+base32hex (upsert idempotente, sem estado por evento),
hooks afterChange/afterDelete na activity (só campos espelhados, nunca
lança), auto-resync quando calendarId muda, retry na view da agenda,
pill de estado no header + FAB + diálogo (4 estados). Cancelado no Teqo
deleta o evento no Google. Sem participantes no v1 (decisão de produto).
Sem migration nova de tabela (hand-written). Closes #635
2026-08-11 13:53:51 -03:00
Francisco Solla
b42de3e7ec feat(C117): ordenação por coluna (header + omnibox) e filtros de ausência em Pessoas
- sort/dir/ausencia no contrato de URL de /campanha/pessoas (molde territórios/B15)
- sortPeopleRows sobre o conjunto filtrado, nulls no fim, tiebreak nome→contactID
- PeopleSortableHead (CampaignSortableHead) com aria-sort nas 7 colunas visíveis
- omnibox: grupo Ordenação (chave×direção) e facet Ausência (Sem assessor/base/contato)
- clear preserva o sort; tests unit + e2e; sem migration/collection
2026-08-11 13:33:21 -03:00
Francisco Solla
c83bd779d2
Merge pull request #665 from fsolla/C112-multiplos-telefones-por-pessoa
C112 — Múltiplos telefones por pessoa
2026-08-11 14:27:47 +00:00
Francisco Solla
6de8fad8f6 feat(C112): múltiplos telefones por pessoa — Contact.phones array (ordem = prioridade)
- Contact.phone (text único) → phones (array field Payload, subcampo value):
  fonte única; principal = phones[0]; beforeValidate normaliza, filtra vazios,
  rejeita inválido e duplicado DENTRO da ficha (C111 entre fichas preservado)
- migration add_contact_phones_array: backfill phone → phones[0] (gen_random_uuid)
  antes do drop da coluna + índice em value (hand-edit documentado; snapshot .json
  gerado mantém as colunas C99/C100 já existentes)
- primaryPhoneOf / phoneValuesOf / reorderWithPrimaryPhone em src/lib/phone.ts
  (ordem = prioridade; inline edit = seta o principal preservando o resto)
- findOrCreateContactByPhone aceita phones[] (dedupe pelo primário; reuse faz
  append dos faltantes; locks em todos os números digitados)
- sync conta→ficha (CampaignUser): phone da conta faz APPEND quando a ficha não
  tem (nunca reordena nem remove); tombstone LGPD substitui TODOS os telefones
- convite redemption: telefone digitado vira principal, demais preservados
- import CSV: preview sinaliza compartilhado também por número secundário;
  bulk insere contact_phones (JOIN nas leituras, uuid no insert)
- busca por telefone (apoiadores, pickers, combobox) casa qualquer número
- UI: PhonesFieldEditor compartilhado (repetível mascarado, remover, ↑/↓,
  modo inline com Salvar) nas 5 superfícies — liderança (form/ficha/lista),
  apoiador (form/ficha), dobradinha (ficha), assessor (ficha), wizard
- UI pública (petição/WhatsApp) intocada: zod e forms ficam com phone;
  as ações mapeiam phone → phones[0]
- testes: spec novo campaignMultiPhones (aceite completo), specs C111/C99
  atualizados, e2e wizard/apoiador ajustados
2026-08-11 10:47:02 -03:00
Francisco Solla
5577d31c38 fix(C113): feed iCal da agenda vivo — contrato de frescor no lugar do snapshot congelado
Spike no build de produção provou que o Next 15 não congela route handlers GET
(rota dinâmica por default); o freeze era o contrato de resposta
(public, max-age=3600 sem validadores) permitindo caches compartilhados
reterem o snapshot do primeiro fetch. Fix: force-dynamic explícito +
buildICalFeedResponse com public no-cache, ETag forte sha256, Last-Modified,
304 só por If-None-Match (nunca por data — blind spot de exclusão), dica
X-PUBLISHED-TTL no corpo do VCALENDAR (RFC 7986) e 404 no-store para
revogação fail-closed no CDN. Testes unit/int/e2e (dev + prod build).
2026-08-11 08:42:42 -03:00
Francisco Solla
170ce59ef4
Merge pull request #640 from fsolla/C111-telefone-nao-unico-duas-pessoas-podem-compartilhar-o-me
C111 — Telefone não único: duas pessoas podem compartilhar o mesmo celular
2026-08-11 03:58:55 +00:00
Francisco Solla
162a518416 feat(C111): telefone não único — duas pessoas podem compartilhar o mesmo celular
Contact.phone deixa de ser único em enforcement (nunca houve constraint DB):
- remove o hook enforceUniqueContactPhone e os asserts assertContactPhone*
  (conflicts/ambiguity) — os 5 fluxos de escrita de pessoa salvam com número
  compartilhado; rename contactPhoneInvariant.ts -> contactPhoneLocks.ts
  (locks passam a significar serialização, não unicidade)
- findOrCreateContactByPhone: 0 fichas cria, 1 reusa (dedupe C6/C99),
  2+ cria ficha nova (a identidade da pessoa em criação é a ficha nova)
- convite autofill/login: ficha ancorada pode compartilhar o phone; o lado
  conta permanece fail-closed (username DB-unique; check de 2+ contas)
- import CSV: preview sinaliza telefone_compartilhado (resolução manual);
  confirm/bulk abortam fail-closed se um phone ganhar 2+ fichas (race)
- fix de achado de execução: supporter_import_batch.actor_id NOT NULL com
  FK ON DELETE set null derrubava o delete de campaignUser com batch
  órfão (23502) — CampaignUser.beforeDelete limpa batches + fixture
  cleanup descobre/deleta batches dos accounts owned
2026-08-10 23:03:05 -03:00
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
Francisco Solla
e75148f3c4
Merge pull request #634 from fsolla/push-optin-automatico-fix-prompt-nativo
feat(push): opt-in automático e fix do prompt nativo no /campanha
2026-08-11 01:07:15 +00:00
Francisco Solla
3e9390230b feat(B197): e-mail oculto por padrão nas listas de pessoas — default B17 nas 5 listas + coluna própria em pessoas/apoiadores + picker em assessores 2026-08-10 21:57:52 -03:00
Francisco Solla
e86331fe4d
Merge pull request #624 from fsolla/OPS37-container-postgres-compartilhado-e-recriado-por-outro-
fix(OPS37): db:start idempotente — não recria o container Postgres compartilhado
2026-08-11 00:54:38 +00:00
Francisco Solla
c4212b3e03 feat(push): opt-in automático e fix do prompt nativo no /campanha
- requestPermission agora é chamado sincronamente no clique: dentro do
  startTransition(async ...) o navegador perdia a user activation e o Chrome
  auto-negava silenciosamente ("Permissão de notificação negada" sem prompt)
- CampaignPushOptInToast no shell (app): staff com permission 'default', sem
  inscrição ativa, consentimento publicado e VAPID configurado; dismiss em
  localStorage; iOS só com app instalado
- cerimônia de ativação/desativação extraída para
  src/utilities/campaignPushClient.ts, compartilhada com o card de perfil
- mensagens distintas para denied (reativar nas configurações do site) vs
  default (prompt não exibido); pin de top-level registrado
- CHANGELOG-AGENTS.md: entrada única
2026-08-10 21:14:56 -03:00
Francisco Solla
c4865135b4
Merge pull request #633 from fsolla/C110+-ux-pos-critique-refinamentos-do-preview-do-arrasto-da-
C110+ — UX pós-critique: preview do arrasto — indicadores ancorados ao frame, gradiente deduplicado, e2e prev
2026-08-11 00:14:04 +00:00
Francisco Solla
7c17d43723 fix(OPS37): db:start idempotente — não recria o container Postgres compartilhado (no-op quando saudável; --force-recreate explícito) 2026-08-10 20:40:36 -03:00
Francisco Solla
9cb8fef98d feat(C110+): preview do arrasto — indicadores ancorados ao frame, gradiente deduplicado, e2e do direction prev
- scene posicionado único (.activity-agenda-swipe-scene) envolve frame + dots/bars:
  percentuais resolvem contra o box do frame, não o content (chevron/label inclusos)
- width: 100% no scene (fix do /simplify: align-items flex-end do prev colapsava a 1px)
- --swipe-frame-columns deduplica o gradiente de 14.285% (2 call sites)
- unit test estrutural do preview (month/day/week/list, chevron por direção)
- e2e novo: direction prev com frame visível (pina o fix do colapso)
2026-08-10 20:38:04 -03:00
Francisco Solla
712d40f83f feat(B195): finalizar wizard Registrar pedido — campo único + título derivado por IA + edição no detalhe 2026-08-10 20:15:05 -03:00
Francisco Solla
f460dac6cf
Merge pull request #623 from fsolla/OPS33-worktree-next-claima-deterministicamente-a-issue-e-abr
OPS33: worktree next claima deterministicamente a Issue (--issue N, reopen) e launch com /work-issue --issue N
2026-08-10 23:08:33 +00:00
Francisco Solla
f0f5b73f3f OPS33: worktree next claima deterministicamente a Issue (--issue N, reopen) e launch com /work-issue --issue N
Claim extraído para scripts/lib/agent-github.mjs (claimIssue/claimBriefLines/
claimQueueEntry/claimTargetVerdict), agent-claim.mjs vira CLI fino com mesma
saída; branch validada antes do claim (sem claim órfão); hint de falha
pós-claim; guard -B contra reset de commits de sessão; diretiva launch com
prompt citado + tokenização xargs no worktree.sh; --issue sem valor/posicional
morrem com motivo (sem misclaim); unit novos (directive com issueNumber,
claimQueueEntry, claimTargetVerdict); docs/textos (AGENTS.md, /worktree, skill
worktree-next-issue, AGENT-OPS, CHANGELOG).
2026-08-10 19:46:54 -03:00
Francisco Solla
1f31d1b628 test(OPS35): modo e2e full-stack sem browser (paradigma HTTP) + migração da família Territórios (Closes #600) 2026-08-10 19:30:29 -03:00
Francisco Solla
0047d2637c ops28(OPS28): worktrees rodam o seed mínimo no provisionamento — e2e local espelha a CI
- scripts/worktree.mjs: provision() aplica migrate + db:seed:minimal nos
  bancos dev e test (paridade com a CI, que roda o par antes do e2e);
  --no-migrate pula os dois (o seed depende do catálogo migrado); fallback
  sem Docker também migra+seeda o teqo/teqo_test compartilhado (com warning
  de sobrescrita dos pins).
- Guard de regressão tests/e2e/seedParity.setup.e2e.spec.ts no projeto setup:
  falha cedo com mensagem clara se o banco de teste não estiver seedado;
  evidência = users do seed (MINIMAL_CAMPAIGN_USERS), não os pins de priority
  — o cleanup dos fixtures e2e reseta priority por round-robin persistente.
- scripts/lib/test-affected-core.mjs: scripts/worktree.mjs em HIGH_RISK_EXACT.
- Docs: AGENTS.md, skill local-database, CHANGELOG-AGENTS.md, impl plan.

Closes #584
2026-08-10 16:11:27 -03:00
Francisco Solla
91a0aca5f0
Merge pull request #613 from fsolla/OPS34-ci-e2e-paralelizar-o-job-cadeia-serial-so-em-dev-shard
refactor(OPS34): fixes do /simplify — fallback e2e_shards, timeout webServer, pin anti-drift
2026-08-10 15:58:10 -03:00
Francisco Solla
4ae8f96242 refactor(OPS34): fixes do /simplify — fallback e2e_shards, timeout webServer CI-only, pin anti-drift, docs 2026-08-10 15:37:11 -03:00
Francisco Solla
97be0c28d6
Merge pull request #611 from fsolla/OPS34-ci-e2e-paralelizar-o-job-cadeia-serial-so-em-dev-shard
chore(OPS34): e2e CI em 2 shards e cadeia de projetos dev-only
2026-08-10 15:07:51 -03:00
Francisco Solla
4baa617c01
Merge pull request #610 from fsolla/C110-agenda-mobile-feedback-visual-do-arrasto-reveal-do-peri
C110 — feedback visual do arrasto na agenda mobile (reveal do período adjacente + commit/snap-back)
2026-08-10 15:02:53 -03:00
Francisco Solla
3b81431d39 refactor(C110): simplify — helpers de dominância/clamp, seed e2e generalizado, CSS sem regras mortas 2026-08-10 14:42:54 -03:00
Francisco Solla
f955c6a7fe chore(OPS34): e2e CI em 2 shards e cadeia de projetos dev-only (Closes #599) 2026-08-10 14:39:39 -03:00
Francisco Solla
9dd8022b6a feat(OPS31): worktree plan abre o opencode com /plan-issue já enviado 2026-08-10 14:36:27 -03:00
Francisco Solla
b23a323f83 feat(C110): feedback visual do arrasto na agenda mobile — reveal do período adjacente + commit/snap-back 2026-08-10 13:51:55 -03:00
Francisco Solla
cc7dff88c4 feat(C100): lista unificada de pessoas em /campanha/pessoas
Merge por Contact (uma linha por pessoa física) de lideranças,
dobradinhas e staff, com capacidades como colunas; omnibox com
capacidade/município/apoio + filtros salvos (2º call site do B18 via
factory createSavedFilterStore); apagar pessoa com manifest de
confirmação e cascata transacional; mobile edge-to-edge (omnibox sem
bordas, cards com divisórias, salvar filtro no header). Sem migration.

Closes #495
2026-08-10 13:45:46 -03:00
Francisco Solla
89e7292529
Merge pull request #580 from fsolla/OPS22-ci-main-e2e-da-agenda-mobile-vermelho-desde-o-merge-do
OPS22 — fix: settle do chat desktop não vaza drawer para a visita mobile da mesma aba (CI main / deploy desbloqueado)
2026-08-10 13:35:07 -03:00
Francisco Solla
0fefbd0490
Merge pull request #604 from fsolla/OPS29-busca-global-sem-sugestoes-mostra-regiao-em-branco-est
fix(OPS29): estado vazio honesto na busca global sem sugestões curadas
2026-08-10 13:31:03 -03:00
Francisco Solla
5bc154dd20 fix(OPS22): settle do chat desktop não vaza drawer para a visita mobile da mesma aba — CI main verde (deploy desbloqueado) 2026-08-10 13:12:12 -03:00
Francisco Solla
fb7f85cd59 fix(OPS29): estado vazio honesto na busca global sem sugestões curadas (Closes #585)
- HomeSearchResultsShell renderiza 'Nenhuma sugestão ainda — priorize
  municípios na planilha de projeção' quando o suggest responde success
  sem hits (Início e FAB overlay compartilham o shell; SSR initialSuggest
  e POST cobertos); gateada por !isFetching para não falar sob o skeleton
- B126 e2e determinístico: pin um município priority alta claimado;
  testes OPS29 novos usam advisor sem carteira (scoped, imune a alta
  global de workers paralelos); settle de animação do overlay no B126
- unit: 3 casos novos no shell + overlap skeleton/mensagem
2026-08-10 12:54:16 -03:00
Francisco Solla
880235ce0f
Merge pull request #588 from fsolla/OPS27-worktree-new-worktree-generico-sem-funcao-pre-definida
OPS27: worktree new — worktree neutro sem função pré-definida
2026-08-10 11:57:48 -03:00
Francisco Solla
2bbc6a403e style(OPS27): prettier em AGENTS.md e worktree spec 2026-08-10 11:29:15 -03:00
Francisco Solla
06d34ed23b OPS27: worktree new — worktree neutro sem função pré-definida (work/<bag>) 2026-08-10 11:24:52 -03:00
Francisco Solla
b210939226 refactor(B193): simplify fixes pós-revisão — mapper único de updates, API de trigger uniforme, grupos de relação extraídos, hardening do sheet host
- Extrai toMunicipalityUpdateViewModel/loadUpdateAuthorNames compartilhados
  (feed + batch da lista); remove avatarClassName morto.
- MunicipalityRelationTriggerProps compartilhado nos 3 wrappers; triggerClassName
  no ExpectedVotesControl; ChipLabel/RelationGroup/CityDash no card.
- Sheet host: cleanup de unmount no overlay (sem drawer órfão), registerFooter
  valida open, branch nulo do handleOpenChange limpa footer; chrome freeze
  documentado.
- A11y/UX: ícone de alerta no rodapé frio (WCAG 1.4.1), iniciais de avatar
  aria-hidden, chip sem_base sem target morto, label da cidade com Tendência,
  aria-label da barra anuncia os 3 cenários, card layout 'colocação · votos'.
- E2E: settleStream no B184 (flake da cópia transitória do streaming), cards
  ancorados por hasText (q= casa prefixos), cor da borda via classe do token.
2026-08-10 11:23:49 -03:00
Francisco Solla
3d8dbae61f feat(B193): card de município mobile denso com edit-where-you-see e rodapé de atualização expandível
- Card denso (wireframe): posição de voto à direita, barra de cenários com
  marcador do ativo, chips classe/tendência/nível (label à esquerda), 3 grupos
  de avatares com wrap (um por entidade), rodapé Última atualização + chevron
  que expande revelando o último card de atualização + CTA registrar; sem
  atualização → CTA direto; prioridade = borda lateral direita 6px primary.
- Controles ganham prop trigger/triggerClassName (precedente RelationChipCell);
  MunicipalityRelationAvatarStack ganha modo wrap.
- C109 (sheet quebra ao abrir) corrigida: sessões do sheet host keyed por id
  (imune à identidade dos callbacks recriados por render) e footer customizado
  renderizado pelo host dentro da árvore do Drawer (portal perdia o Dialog
  context — reproduzido digitando no sheet).
- Dado: MunicipalityListViewModel.lastUpdate (batch por página, scoped).
- E2E novos (barra/votos, chip nível, borda prioridade, expansão/CTA vazio).
Sem migration/access/Consent.
2026-08-10 11:23:49 -03:00
Francisco Solla
da20ed6190 refactor(OPS26): simplify fixes do /simplify — marcador TEQO_ prefixado, helper printLaunchDirective, IFS pinado no launch, docs reconciliadas 2026-08-10 10:58:55 -03:00
Francisco Solla
344fe843df feat(OPS26): worktree next/plan no terminal abrem o opencode com presets (modelo deepseek-v4-flash, auto-approve, skill inicial) 2026-08-10 10:51:19 -03:00
Francisco Solla
7a4279b281 feat(OPS25): comandos opencode /work-issue e /plan-issue como atalhos diretos às skills
Dois comandos novos (.opencode/commands/) que instruem o agente a
carregar a skill de mesmo nome pela ferramenta de skills e seguir o
fluxo dela ponta a ponta, com $ARGUMENTS repassado. Frontmatter com
description (autocomplete do TUI) e model deepseek/deepseek-v4-flash
(alinhado ao launch da OPS26). Guard unit pina o acoplamento
comando↔skill para renomes não quebrarem silenciosamente.
2026-08-10 10:25:26 -03:00
Francisco Solla
972c6ac8ba
Merge pull request #577 from fsolla/D7-sticky-dos-headers-da-tabela-desktop-quebrado-b41-morto-t
fix(D7): sticky dos headers das tabelas desktop volta a engajar (overflow-x-clip)
2026-08-10 09:52:54 -03:00
Francisco Solla
bc661821c8
Merge pull request #574 from fsolla/B192-sollinha-follow-ups-sugeridos-apos-cada-resposta-instru
B192 — Sollinha: follow-ups sugeridos após cada resposta (instruídos no prompt, extraídos pela UI)
2026-08-10 09:37:45 -03:00
Francisco Solla
50aa11eb95
Merge pull request #575 from fsolla/B186-sollinha-prioridades-do-momento-gestao
B186 — Sollinha: prioridades do momento (getMunicipalityPriorities)
2026-08-10 09:20:18 -03:00
Francisco Solla
2fa38a08f5 style(B192): format (prettier) 2026-08-10 09:13:38 -03:00
Francisco Solla
382c4c46ec feat(B192): follow-ups sugeridos após cada resposta do Sollinha (bloco no prompt + chips extraídos) 2026-08-10 09:13:38 -03:00
Francisco Solla
00a4b7f0c7 fix(D7): sticky dos headers das tabelas desktop volta a engajar (overflow-x-clip)
O wrapper do ui/Table (`overflow-x-auto`, depois `overflow-x-hidden` via
container queries) é um scroll container nos dois eixos — o overflow-y computa
para `auto` — e o th `position: sticky; top: 0` declarado pelo B41 resolvia
contra ele, que nunca rola (o scroll vertical real é o CampaignContentScroll da
shell). A feature estava silenciosamente morta desde o B41.

`overflow-x: clip` (permitido ao lado de `overflow-y: visible`, sem promover
visible→auto) NÃO cria scroll container: o th volta a resolver contra o
scrollport da shell e engaja. Troca nas duas listas com sticky declarado
(MunicipalityList, TerritoryList), preservando o fallback `overflow-x-auto`
para browsers sem container queries. As demais listas (lideranças, demandas,
dobradinhas, apoiadores) estouram em 1440px e dependem do scroller — não
tocadas.

Validado nos 3 engines (Chromium/Firefox/WebKit): th 174 → 69 (topo do content
box do scrollport, padding da shell acima — mesmo ponto onde o omnibox mobile
do B184 cola). E2E novo pina o engajamento com asserção bilateral + guarda de
computed style; unit pinado no novo contrato; JSDoc do containerClassName
atualizado. Closes #543.
2026-08-10 09:11:38 -03:00
Francisco Solla
e9d7f0da39
Merge pull request #567 from fsolla/C109-quick-edit-sheet-mobile-da-lista-de-municipios-quebra-a
fix(campanha): quick-edit sheet mobile com footer custom quebrava ao abrir (C109)
2026-08-10 08:49:39 -03:00
Francisco Solla
b354ad55d6 style(B186): simplify fixes — NaN tiebreak, janela simétrica, testes sem bomb-relógio
- byGravity estagnação: Infinity - Infinity (NaN) matava o tiebreak para dois
  municípios nunca-sinal (topo da lista) — comparação explícita + teste
- fronteira da janela: módulo puro inclusivo (>= cutoff) e query da tool em
  greater_than_equal (antes greater_than, assimétrico) + testes 30/31 dias
- ultimoSinalAtrasDias reporta a idade da atualização decisiva no balde sinal
  (E9 podia devolver null ao lado de evidência 'há 2 dias' em linha stale)
- fixtures da tool com datas relativas a Date.now() (absolutas apodreceriam
  em ~30 dias); assert de user: ctx.user no caminho do assessor
- options type em inglês (windowDays/reason/sortBy); MunicipalityPriorityReason
  não exportado (knip); agora passado da tool (um relógio)
- expectedVotes select estreito { central: true }; sort '-createdAt' removido
  (módulo re-deriva o decisivo); NARROW_SCOPE_HINT compartilhado em aiToolScope
- baseResponse unifica o shape (short-circuit + retorno); docblock órfão da
  extração removido; prompt: coordenação/candidatura
- impl plan: registro de execução + shape entregue
2026-08-10 08:37:49 -03:00
Francisco Solla
11886a653d feat(B186): Sollinha — prioridades do momento (getMunicipalityPriorities)
Tool nova do chat que responde "quais devem ser minhas prioridades neste
momento?" com top N municípios do escopo, cada um com uma linha de
evidência e o critério declarado. Ranking derivado na hora em módulo puro
(municipalityPriorities.ts): 3 baldes por gravidade (sinal desfavorável
recente > estagnação > potencial alto com nível baixo), exclusão de
atualização recente favorável, frescor E9 (max lastUpdateAt/lastPledgeAt),
potencial = expectedVotes.central com fallback nos válidos 2022 (artefato).

Resolver de escopo da B185 extraído para aiToolScope.ts (2º consumidor;
getPendingLeaderships refatorado sem mudança de comportamento). Gate staff
fail-closed, leituras overrideAccess: false + user. Seção nova no system
prompt. 38 unit tests novos; 1772 unit + 680 int verdes; build OK.
2026-08-10 08:37:49 -03:00
Francisco Solla
2b3c48e063 refactor(B189): simplify — nomes alinhados à B186, loader de nomes compartilhado, guard de escopo vazio
- aiToolScope.ts: AIToolScope/AIToolScopeKind/AIToolScopeMunicipality
  (mesmos nomes da extração da B186 em voo — conflito de merge vira trivial)
- loadNamesByIds da B185 e loadAdvisorNames da B189 fundidos em
  aiToolQueries.ts (loadAIToolNamesByIds) — don't twin
- guard fail-closed de escopo vazio (assessor fora do portfólio) + teste
- sortStale sem ?? 0 (partição garante ISO) — type honesty
- prompt: exemplo de Salvador com limiar estrito; index ordenado
2026-08-10 08:12:00 -03:00
Francisco Solla
6027aac941 feat(B189): Sollinha — municípios sem atualização recente (tool getMunicipalitiesWithoutUpdate)
Tool nova da família B185/B186: cobertura do acompanhamento por recência
de municipality.lastUpdateAt (derivado dos hooks de municipalityUpdate),
limiar default 30 ajustável, lista exaustiva com total, nunca atualizados
no topo, assessores por item. Resolver de escopo extraído para
aiToolScope.ts (2o call site; B186 sera o 3o). Gate staff fail-closed;
unit tests 14 novos + regressao B185 verde.
2026-08-10 08:12:00 -03:00
Francisco Solla
5ad2f7c722 fix(campanha): quick-edit sheet mobile com footer custom quebrava ao abrir (C109)
O footer custom ('Registrar atualização' B42, 'Registrar movimento' E14) era
portalado da célula para o DrawerFooter do shared-sheet: createPortal preserva
o contexto React da origem (sem Drawer.Root), então o DrawerCloseButton
(base-ui DialogClose) crashava em useDialogRootContext() e o sheet nunca
renderizava.

O footer agora viaja no chrome do CampaignListSheetHost (mesmo mecanismo de
title/description) e é renderizado inline no DrawerFooter — contexto correto
por construção. O elemento continua criado na célula (estado do form e
associação form attribute preservados). footerPortalRef/attachFooterPortal/
hasCustomFooter removidos; fallback sem provider e popover intactos.

Cobertura: unit do shared path (abre/fecha/reabre + submit via form attribute)
e e2e mobile no describe B42 (abre, registra, fecha — incluindo o sheet do
nível E14).
2026-08-10 08:11:05 -03:00
Francisco Solla
733b019f2b
Merge pull request #560 from fsolla/B190-sollinha-municipios-sem-dobradinha-dobradinhas-orfas-co
B190 — Sollinha: municípios sem dobradinha + dobradinhas órfãs (cobertura de parcerias)
2026-08-10 07:35:01 -03:00
Francisco Solla
7bdd7f91b1
Merge pull request #559 from fsolla/B187-sollinha-links-com-aparencia-de-link-nas-respostas
B187 — Sollinha: links com aparência de link nas respostas
2026-08-10 07:34:36 -03:00
Francisco Solla
4373599192 feat(B190): Sollinha — cobertura de parcerias (municípios sem dobradinha + dobradinhas órfãs)
Nova tool getPartnershipCoverage com dois modos: municípios do escopo sem
nenhuma dobradinha vinculada (Salvador agrupada por cidade, drill-down por
ZE) e dobradinhas órfãs (sem município vinculado no registro inteiro, via
bypass agregado-only do helper B34). Gate fail-closed para leader; região
tolerante a acento; critério declarado na resposta. buildCampaignLinks ganha
o filtro stateDeputies sem_dobradinha na lista de municípios (URL B176).

Closes #527
2026-08-10 07:24:15 -03:00
Francisco Solla
1016668265 feat(B187): links com aparência de link nas respostas do Sollinha
Estilo de links no markdown do chat (primária + sublinhado + hover/foco),
externos abrem em aba nova; compõe com o next/link interno do B188.
Sem migration; e2e + unit cobrem a superfície.
2026-08-10 07:20:44 -03:00
Francisco Solla
74ccce432d
Merge pull request #554 from fsolla/B191-sollinha-acoes-rapidas-de-abertura-no-chat-chips-de-per
feat(B191): chips de pergunta de abertura no chat Sollinha
2026-08-10 04:15:38 -03:00
Francisco Solla
04951f6772 feat(B191): chips de pergunta de abertura no chat Sollinha (curados por papel e viewport)
- src/lib/sollinhaOpeningQuestions.ts: catálogo puro de chips de abertura
  (staff: 4 perguntas ancoradas nas tools atuais; leader: conjunto seguro
  sem tool eleitoral — fail-closed B180)
- ChatChipGroup: primitivo presentacional reutilizável (slot compartilhado
  com o B192)
- role do usuário flui do layout server pelo shell até o context do chat
- slot acima do input renderiza só com conversa vazia (messages 0 + ready);
  tocar envia via sendMessage e os chips somem no primeiro envio
- unit tests do catálogo + e2e (staff desktop 4 / mobile 3 / leader seguro)
2026-08-10 03:49:08 -03:00
Francisco Solla
acd522f035 feat(C103): agenda mobile — formulário de criação usável (sheet do topo, sem labels, salvar fixo, seletor em bottom sheet) 2026-08-10 03:34:33 -03:00
Francisco Solla
d60fe3867a
Merge pull request #552 from fsolla/C107-atualizacoes-mobile-sheet-de-criacao-usavel-altura-auto
C107 — Atualizações mobile: sheet de criação usável (altura auto até o topo, form sem labels com divisórias, placeholders, polaridade em toggle, checks à direita)
2026-08-10 03:29:43 -03:00
Francisco Solla
7cf820f51b fix(B185): revisão /simplify — escopo efetivo de assessor, chips sem vazamento, DRY e convenções
- Município/território real fora do portfólio devolve lista vazia escopada (antes
  erro 'não reconhecido') + escopoRestrito: true declarado na resposta
- Chips de município fora do acesso omitidos (nunca id pelado)
- uniqueRelationshipIds canônico no lugar do uniqueIDs local
- Chaves do input zod em inglês (scope/filter/mode); Salvador sem round-trip
- Pledge query restrita a lideranças engajado + dica quando truncado + prompt
2026-08-10 03:07:13 -03:00
Francisco Solla
5f1a20de54 test(C108): asserções assíncronas no spec do bell — itens renderizam após a action resolver (robustez sob carga) 2026-08-10 02:56:21 -03:00
Francisco Solla
c3f10cce23 feat(C108): sino de notificações — abrir marca tudo como lido, painel limpo, modal central no desktop
openCampaignNotifications (lista + marca em 1 round trip) substitui
listCampaignNotifications + markAllCampaignNotificationsRead; markAllNotificationsRead
extraída (owner-scoped, idempotente, cap 200) com int test; bell vira Dialog
desktop / Drawer mobile (par C94), sem título/rodapé/botões, skeleton de
loading, safe-area, focus-visible, role=alert; readAt sai do contrato do item;
P4-D ledger atualizado (batch sem transação agora no hot path, self-healing).
2026-08-10 02:56:20 -03:00
Francisco Solla
07464b8810
Merge pull request #548 from fsolla/B188-sollinha-contexto-da-conversa-persiste-na-sessao-da-jan
B188 — Sollinha: contexto da conversa persiste na sessão da janela/tab
2026-08-10 02:53:54 -03:00
Francisco Solla
03a2f57d03 feat(B185): tool do Sollinha para lideranças pendentes de abordagem + assessores responsáveis
getPendingLeaderships: escopo por território/cidade(Salvador=19ZE)/município com
folding de acento, critério de pendência declarado (status A abordar/Em disputa
ou Engajado sem votePledge no escopo; negativo excluído na query), filtro
sem_assessor (C3), modo municipios_sem_lideranca (C4), nomes de assessores via
query própria, gate staff fail-closed (leader deny antes de qualquer query).
Sem migration/Consent/UI; unit tests 14 novos; allowlist P3-D justificada.
2026-08-10 02:27:04 -03:00
Francisco Solla
c8e009b47a
Merge pull request #537 from fsolla/C104-agenda-compromisso-todo-o-dia-na-criacao-rapida-so-data
C104 — Compromisso 'Todo o dia' na criação rápida da agenda (só datas, multi-dia)
2026-08-10 01:37:56 -03:00
Francisco Solla
5380154dca fix(B188): simplify — settle não reabre chat fechado no reload desktop; prune com cabeça de pergunta; node fora do Link; mock SSE v7 no B167
- P1: B167 settle forçava open=true no load desktop mesmo com sessão restaurada
  open:false (storage auto-curava para true); restore agora vence quando existe
- prune: cabeça assistant quebra convertToModelMessages; drop até ancorar em user
- MarkdownLink: node do react-markdown não vaza para <Link>/<a>
- isUIMessage: parts com shape inválido falham-closed no restore
- B167 mock: chunk legado {"type":"text"} falha schema v7 (status preso em erro);
  agora usa start/text-start/text-delta/text-end/finish
2026-08-10 01:34:53 -03:00
Francisco Solla
9c1d4839b2
Merge pull request #539 from fsolla/agent/C101-ux-agenda-mobile-affordance-do-hoje-no-titulo-navegacao
C101-ux — Agenda mobile: navegação por teclado + affordance do tap no título
2026-08-10 01:17:05 -03:00
Francisco Solla
ca1dbb80f7 feat(B188): contexto da conversa do Sollinha persiste na sessão da janela/tab
- sessionStorage por aba: mensagens + estado aberto, restaurados no reload;
  escrita só em estado settle (nunca mid-stream); guardrail 50 msgs / 256 KB
- links internos (/campanha…) do markdown navegam via next/link (sem reload)
- e2e novo + ajuste do B167 (visita mobile nova agora loga em viewport mobile)
2026-08-10 01:13:25 -03:00
Francisco Solla
ea700e3113 feat(C104): compromisso 'Todo o dia' na criação rápida da agenda (só datas, multi-dia) 2026-08-10 01:13:12 -03:00
Francisco Solla
4e920ee064 feat(C107): sheet de criação de atualização usável no mobile — altura auto até o topo, form sem labels com divisórias full-bleed, placeholders, polaridade em toggle, checks à direita
- MunicipalityUpdateFields ganha layout 'labeled'|'list'; polaridade vira ToggleGroup Ruim|Neutra|Boa nas duas resoluções (mesmo dado, hidden input)
- layout 'list': labels sr-only, textarea sem borda, divisórias divide-y full-bleed, Urgente/Sinalizar adversário como linha tocável com check à direita
- StrictCombobox: prop placeholder (backward-compatible)
- Sheet do feed: max-h-dvh (teto no topo, rola por dentro), título sr-only, form lista com placeholders descritivos; desktop preservado
- Quick-edit B42 herda o visual no sheet mobile via layout
- unit/e2e atualizados para o toggle (radio)
2026-08-10 01:11:24 -03:00
Francisco Solla
8c79eddf96
Merge pull request #536 from fsolla/C105-agenda-tags-para-agrupar-compromissos-filtro-por-tag-na
feat(C105): tags na agenda — sheet, rótulo no evento e prefill do Mais detalhes
2026-08-10 00:58:56 -03:00
Francisco Solla
f3c41d3382 feat(C101-ux): agenda mobile — navegação por teclado (região focável) + affordance do tap no título
- useAgendaKeyboardNavigation: ArrowLeft/Right → prev/next no mobile
  (FullCalendar v7 não tem setas no grid — verificado no bundle); skips
  para modificadores/dialog/menu/inputs; restore de foco na região
- Região de teclado mobile-only: tabIndex/role/aria-label gateados por
  isMobile (desktop mantém a toolbar e sua árvore a11y intacta)
- onTitleClick ganha icon?: LucideIcon (glyph da página, aria-hidden);
  agenda passa RotateCcw como affordance do 'voltar para hoje'
- E2E: teste de teclado (dia e mês), glyph no título, hardening do swipe
  CDP (pacing 25ms — burst era classificado como flick pelo Chromium e
  disparava back-nav ~35% das runs)
- Unit: 14 testes do hook/pura + 4 do renderer do glyph; impl plan
  docs/plans/c101-ux-pos-critique-impl.md (aprovado pelo humano)

Closes #512
2026-08-10 00:53:19 -03:00
Francisco Solla
cd41b44de4 feat(C105): tags no sheet da agenda + rótulo no evento + prefill no "Mais detalhes"
O filtro por tag na omnibox já existia (C94); o C105 completa a superfície da
agenda: campo de tags no overlay de criação inline (chips, sugestões das tags
já usadas), rótulo leve #tag no evento do calendário (oculto no mês/mobile) e
tags via prefill do "Mais detalhes" (params repetidos, parse fail-closed).

- ActivityTagInput extraído do ActivityForm (compartilhado, aria-label novo)
- "Mais detalhes" espelha estado vivo (horários editados + tags)
- popover do sheet com max-h --radix-popper-available-height (shift-before-flip
  do Radix clipava os campos do topo em viewports baixos)
- submit lê tags do estado espelho (fonte única), 4 KB cap p/ responsáveisJson
- limites mantidos 20/80 (C14), sem migration/access/Consent

Impl: docs/plans/agenda-tags-eventos-impl.md (aprovado no gate humano)
2026-08-10 00:21:20 -03:00
Francisco Solla
c2ddbf6006 refactor(D6): simplify — find endpoint-only no subscribe, sslmode verify-full, guards no check-push-chain 2026-08-10 00:01:46 -03:00
Francisco Solla
f4873b8b3f fix(D6): push no device ponta a ponta — SW dinâmico, subscribe idempotente, check-push-chain
- /campanha/sw.js force-dynamic: buildId VERCEL_DEPLOYMENT_ID em runtime
  (deploys CLI --prebuilt não têm envs de build; prod servia campanha-dev)
- subscribeCampaignPush find-before-create (endpoint UNIQUE — 2º toque
  estourava com erro falso)
- scripts/check-push-chain.mjs + pnpm db:check:push-chain (read-only,
  veredito por elo: SW/envs/consent/inscrições/eventos)
- VAPID envs adicionadas no Vercel production (ops)
- impl plan + CHANGELOG-AGENTS
2026-08-09 23:41:00 -03:00
Francisco Solla
a7dce2c716
Merge pull request #516 from fsolla/C101-agenda-mobile-calendario-com-cara-de-app-nativo-context
C101 — Agenda mobile: calendário com cara de app nativo (contexto no header, navegação por arrasto, filtro sticky, dia na hora atual)
2026-08-09 22:43:12 -03:00
Francisco Solla
51fb715ec7 feat(C101): agenda mobile com cara de app nativo — contexto no header, swipe, filtro sticky, dia na hora atual
- toolbar do FullCalendar some no mobile; título do header mostra o período
  ("9 Agosto", "3–9 Agosto", "Agosto", "Agenda") e tap volta ao hoje
- navegação por arrasto horizontal (useAgendaSwipeNavigation): limiar 48px,
  claim 12px com preventDefault, touchcancel sintético contra o long-press
  do FC, dateClick do dia suprimido (C91/C15 intactos)
- filtro vira strip edge-to-edge sem rótulo, sticky sob a barra do app
  (containing block via :has no padding do scroll container)
- visão de dia com scroll interno (height fixa), 08:00 na abertura,
  cabeçalho do dia fixo, scroll preservado ao trocar de dia
- desktop inalterado; impl plan + plano pós-critique (C101-ux)
2026-08-09 22:09:47 -03:00
Francisco Solla
457aad5a21
Merge pull request #510 from fsolla/C102-mobile-sem-botao-sheet-de-sidebar-navegacao-pela-barra-
C102 — Mobile: sem botão/sheet de sidebar — navegação pela barra inferior + Mais
2026-08-09 19:56:07 -03:00
Francisco Solla
f0dff330bd feat(C102): sem hamburger/sheet de sidebar no mobile staff — navegação pela barra + Mais
No mobile (<768px) staff perde o botão de abrir sidebar na top bar
(CampaignMobileTopBar role-gated via useCampaignPageChromeRole +
isStaffCampaignRole; leader mantém) e o sheet de navegação deixa de
montar (CampaignSidebar early-return quando isMobile && staff).
Filtros salvos de Municípios (B18) migram para o drawer Mais
(MunicipalityNavSavedFilters variant 'overflow', mesma store,
apagar/desfazer/sucessão de foco; drawer ganha overflow-y-auto).

Correção de bug pré-existente (D4): o Toaster vivia dentro do wrapper
[contain:layout_paint] do painel da AI, que vira containing block para
position:fixed e stacking context próprio — toasts com ação (ex.
Desfazer) ficavam inalcançáveis sob os portais z-50 de drawer/sheet.
Movido para o root layout (campaign).

Testes: unit top bar (staff sem trigger / leader com trigger) + e2e
(staff sem trigger/dialog, leader com sheet, B18 no drawer completo).
2026-08-09 19:13:43 -03:00
Francisco Solla
27daa1339e feat(C99): toda conta campaignUser aponta para uma ficha Contact; lideranças ganham assessores responsáveis 2026-08-09 19:12:35 -03:00
Francisco Solla
dff940681d fix(B182): PWA iOS — heal do zoom preso do visual viewport no standalone
Motor client-side (standalone-iOS only, no-op em aba/desktop):
heal imediato no install + launch/bfcache/resume/pós-teclado/resize,
scrollTo(0,0) + re-measure nudge, retry ≤3×250ms por episódio,
pinça preservada (janela renovada por gesture*/touch ≥2), debug
global para verificação em aparelho, fallback maximum-scale off.

40 testes unit (jsdom) pinam detecção, guards, retry e teardown.
2026-08-09 18:50:00 -03:00
Francisco Solla
fdedfa9ad4 feat(C97): seletor shadcn de dia+horário 24h no popover de criação rápida da agenda
Troca o datetime-local nativo (AM/PM no idioma do browser, horário pouco
confiável) por ActivityDateTimeField: trigger com rótulo civil
dd/mm/aaaa às hh:mm + popover com Calendar (react-day-picker 9.7,
timeZone America/Bahia, locale pt-BR) e selects Hora 00-23 / Minuto
em passos de 15. Contrato de valor e submit inalterados; prefill do
slot mantido. Novo ui/Calendar.tsx (variante radix-nova), helpers
puros em campaignTime.ts. Sem migration. Closes #482 (PR).
2026-08-09 15:13:15 -03:00
Francisco Solla
ff53e15e7e feat(B180): gate fail-closed de leader nas tools eleitorais legadas do Sollinha (getTopDeputies/getMunicipalityVotes)
ElectionDataGate compartilhado (3 call sites) + refactor da B177 para o helper.
2026-08-09 14:54:31 -03:00
Francisco Solla
07b4509e42 feat(campaign): Tendência migra para Ações ao colapsar a coluna (B172)
Em /campanha/municipios (staff), abaixo de 60rem do container a coluna
Tendência some inteira (hidden @min-60:table-cell no th+td, invertendo a
decisão B158 'container query nunca duplica' só para Tendência) e o mesmo
editor monta como ícone compacto na coluna Ações, ao lado do de atualização;
acima de 60rem volta o badge completo e Ações é gated fechado quando nada
está oculto manualmente. Ações torna-se sempre montada para staff e pinada
por ocultação manual no B17. Uma instância visível do editor por linha por
faixa (dual-render CSS-gated, sem JS/resize). Sem migration. Closes #446.
2026-08-09 13:36:04 -03:00
Francisco Solla
dff3a9e8b1 feat(C95): seletor de modo de visualização no header do app (agenda)
- view=week|day|month|list no contrato URL da agenda (canônico, fail-closed)
- seletor compacto 'Semana ▾' no header desktop e mobile (slot C94/C95)
- FullCalendar headerToolbar sem os 4 botões de modo; escolha vence resize
- effect normaliza a vista também quando a URL perde o param (sem vista órfã)
- useNarrowMeasured(breakpoint) em hooks/use-mobile (settle de hidratação)
- e2e reescrito no seletor + persistência + escolha-vence-resize + estabilização
  dos waits de slot (grid lazy do FullCalendar substitui gridcells no load)
2026-08-09 12:34:04 -03:00
Francisco Solla
6c34da4a47
Merge pull request #488 from fsolla/B178-salvador-cidade-agregada-no-campanha-pagina-virtual-lin
fix(B178): review do simplify — ordenação de nulls nativos, mapa sem slug da cidade, a11y e testes de borda
2026-08-09 10:53:46 -03:00
Francisco Solla
d8ee2dbafa fix(B178): review do simplify — nulls nativos em DESC, mapa sem slug da cidade, sr-only morto, sinal mobile, paginação/edges testados 2026-08-09 10:40:18 -03:00
Francisco Solla
f1a8332835
Merge pull request #487 from fsolla/B178-salvador-cidade-agregada-no-campanha-pagina-virtual-lin
feat(B178): Salvador como cidade no /campanha — página virtual, linha comum na lista e busca, sem dupla contagem
2026-08-09 10:21:16 -03:00
Francisco Solla
de66fe3e3f feat(B178): Salvador como cidade no /campanha — página virtual, linha comum na lista e busca, sem dupla contagem 2026-08-09 10:07:05 -03:00
Francisco Solla
d5d1d0c30e chore(B181): remove o aviso de janela vazia da agenda
Sem bloco 'Nenhum compromisso nesta janela e neste filtro' nem botão
'Criar atividade' acima do calendário: a criação segue pelo slot click
(C91), botão dos filtros e FAB. buildActivityCreateHref permanece com
2 consumidores. Teste unitário de regressão + impl plan + changelog.
2026-08-09 09:34:25 -03:00
Francisco Solla
b98366ea66 test(C93): fix beforeEach import in dialog unit spec 2026-08-09 09:04:51 -03:00
Francisco Solla
2ba7a88531 feat(C93): allow generating calendar import link without filters
The 'Link de import' button was disabled whenever no agenda filter was
active, forcing a filter to subscribe to the full campaign agenda. The
server already supported the empty-filter case (filterMunicipality empty
= entire creator scope via buildFeedWhere/resolveFeedCreatorAccess), so
this unlocks the button and drops the now-dead state/hasFilters props.
The dialog copy is neutralized ('recorte' -> 'agenda') to cover both the
filtered and filterless shapes.

Int tests pin the content acceptance: coordinator filterless feed covers
all municipalities; advisor filterless feed is scoped to the administered
portfolio (nothing out-of-scope leaks); an advisor with no assigned
municipality serves an empty feed (fail-closed). A new component test
asserts the button is enabled with zero filters and the create flow works.

Closes #437
2026-08-09 09:02:06 -03:00
Francisco Solla
e0530a3a90 feat(C94): agenda omnibox + feed no header/FAB 2026-08-09 04:35:39 -03:00
Francisco Solla
14bbee6e00 feat(B176): filtrar municípios por Dobradinha/Liderança/Partido + coluna Dobradinhas ao assessor
Omnibox de /campanha/municipios ganha as dimensões Dobradinha (nome do
contato), Liderança (nome do contato) e Partido (sigla), com recortes
"Sem dobradinha"/"Sem liderança"/"Sem partido" no precedente de
"Sem nível". Params aditivos stateDeputy/leadership/party na URL
congelada; buildMunicipalityListWhere ganha catálogo request-scoped
fail-closed (municipalityRelationSets) que vira as leituras reversa e
cruzada em where — sem migration, paginação nativa mantida; facets
estendidas e o mapa compartilha o catálogo. Coluna Dobradinhas liberada
ao staff (servidor já scoped B37/B157; só o gate de UI mudou).
Extrato do par sentinela+exists:false em campaignListUrl (5 call sites).
2026-08-09 03:19:54 -03:00
Francisco Solla
5a7f3bbb8a
Merge pull request #475 from fsolla/B177-sollinha-responder-em-quais-cidades-x-foi-o-deputado-ma
feat(B177): Sollinha responde em quais cidades X foi o deputado mais votado (rank competitivo reverso, flexível)
2026-08-09 02:46:17 -03:00
Francisco Solla
cef946ad7d feat(B177): Sollinha responde em quais cidades X foi o deputado mais votado (rank competitivo reverso, flexível) 2026-08-09 02:35:53 -03:00
Francisco Solla
7785c1088c fix(worktree): provisiona PLAYWRIGHT_BASE_URL no .env.test.local — e2e usa porta do slot (OPS20)
O .env.test.local (que o playwright.config.ts lê) passa a carregar
PLAYWRIGHT_BASE_URL na porta do slot (3100+slot); antes só o .env.local
(que o config não lê) o tinha, então dois test:e2e em worktrees paralelos
subiam dev servers na porta 3000. Causa-raiz tratada: os dois blocos env
inline do provision viraram um builder puro único (worktreeEnvFileContents
em scripts/lib/worktree-env.mjs) que deriva NEXT_PUBLIC_SITE_URL +
PLAYWRIGHT_BASE_URL do mesmo devPort para ambos os arquivos — paridade
estrutural, drift impossível + unit tests de paridade. playwright.config.ts
intocado (a variável certa já existia).
2026-08-09 02:19:26 -03:00
Francisco Solla
177803a54a
Merge pull request #470 from fsolla/B173-sollinha-consulta-por-voz-no-chat
B173 — Sollinha: consulta por voz no chat
2026-08-09 02:16:53 -03:00
Francisco Solla
9f997aa232
Merge pull request #471 from fsolla/B175-repintar-a-lista-de-territorios-no-padrao-da-lista-de-m
B175 — Repintar a lista de Territórios no padrão da lista de Municípios
2026-08-09 01:58:06 -03:00
Francisco Solla
4668c018bf feat(B173): consulta por voz no chat da Sollinha (STT → rascunho)
Adiciona entrada por voz ao chat: botão de mic no InputGroup grava no
browser (getUserMedia + MediaRecorder), envia o clip multipart para a nova
rota POST /campanha/api/ai-transcribe (auth cookie + rate limit no mesmo
balde + same-origin) e injeta a transcrição como rascunho editável — o
envio segue o fluxo normal do chat. O áudio nunca é persistido.

STT via Deep Infra whisper-large-v3 (DEEPINFRA_API_KEY, mesma do B174);
a costura de rede vive em utilities/ai/deepInfraTranscribe.ts; a máquina
de captura em useMicTranscript. Falha de permissão/provedor degrada a
mensagem em pt-BR sem quebrar o chat por texto.

Closes #448
2026-08-09 01:47:03 -03:00
Francisco Solla
fe81358fe3 B170 — spec do contrato do colapso de chips + shift único do load SSR deferido documentado
F1: RelationChipCell ganha spec unitário (relationChipCellCollapse) que pina o
colapso medido pelo layout real (16 chips → slice de 3 linhas = 12), expande
pelo 'Ver mais…' e recolhe pelo 'Ver menos', com getBoundingClientRect stubado
por índice — a lacuna que o B169 deixou (specs com measureOverflow:false ou
1 chip) não tinha regressão do comportamento de colapso após a mudança de timing.

F2: medição PerformanceObserver no worktree confirma o resíduo pré-hidratação
do B169 sem regressão material: load SSR→hidratação com shifts únicos
[0.139, 0.043, 0.257] (fontes do shell, nenhuma célula de chips) e ordenação
client-side com zero shifts novos; eliminá-lo exigiria estimativa de largura
(rejeitada) ou mudança de layout/regra (anti-goal) → defer documentado.

Sem migration, dados, access ou mudança visual.
2026-08-09 01:36:59 -03:00
Francisco Solla
e75bb1c2f3 feat(campaignTerritories): repintar a lista de Territórios no padrão da lista de Municípios (B175)
- Colunas responsivas ao painel via @container/territory-list (P0→P3), sem scroll horizontal
- Contagem de municípios no nome ("Irecê (20)"); % e votos absorvidos na célula 2022 (hover = válidos)
- Célula 2026 no detalhe de Municípios (valor central + %, faixa de cenários no hover, leitura)
- Colunas de rede só leitura: Assessor, Liderança, Dobradinha (para todo o staff)
- Cobertura oculta por padrão no seletor; labels curtos (2022/2026/Captura/Cobertura)
- loadTerritoryOverview devolve { rows, references }; rollup puro com soma por cenário, hasEstimate e uniões de rede
2026-08-08 23:58:54 -03:00
Francisco Solla
7343f15053 feat(C89): página Atualizações — feed de cards + filtro omnibox + criar (modal/bottom sheet)
Substitui o placeholder B164 por um feed operacional da carteira (staff):
- Cards com avatar do autor, polaridade/urgente/adversário, município e data
- Filtro omnibox (município via dotted-path, polaridade, urgente, autor, busca)
  no padrão das listas (URL canônica + adapter puro)
- Criação in-place: modal desktop / bottom sheet mobile, reusando o registro
  unificado (C87) com prefill do município do filtro ativo
- Leader lockdown intacto (gate staff + canReadMunicipalityUpdate fail-closed)
- parseMunicipalityUpdateFormData único para os 3 forms de atualização
- Loader cruzando a carteira sem cláusula de município (access por ator)
- Int/unit specs (19 unit + 5 int) + convenções

Fix real: o modal de criação não pode ser filho React do <form> de filtros —
React 19 não liga progressive enhancement em action-form aninhado.
2026-08-08 23:07:23 -03:00
Francisco Solla
77673147ca feat(C91): cria evento inline no calendário da agenda (popover/drawer no slot clicado) 2026-08-08 18:27:25 -03:00
Francisco Solla
63642483a4 feat(C90): remodela responsáveis da Atividade em campo único polimórfico
Um campo responsible (staff campaignUser · leadership · stateDeputy) multi-valor
substitui responsible/Contact + advisors + leadership; assessor mantém acesso via
notação-objeto equals no access; nova migração esquema+dados (advisors/leadership
→ responsible); busca q da lista título-only; notificação notifica staff do campo.
Closes #426
2026-08-08 17:10:48 -03:00
Francisco Solla
8b2bc51aa5
Merge pull request #433 from fsolla/plans/plan-issue
worktree: plan cria um worktree diferente por invocação (paralelismo /plan-issue)
2026-08-08 16:52:26 -03:00
Francisco Solla
6c8c466b40 fix(worktree): plan cria um worktree diferente por invocação (paralelismo /plan-issue)
Cada 'pnpm worktree plan' agora deriva um branch próprio: plans/plan-issue-<bag>
quando um bag é passado (sufixo -2/-3 em colisão) ou o próximo sequencial
plans/plan-issue-<n> quando não. O prefixo minúsculo plans/… nunca colide com o
branch <Code>-<slug> de 'next'. planBranchName (scripts/lib/worktree.mjs)
unit-testado; docs e shell/command atualizados.
2026-08-08 16:40:05 -03:00
Francisco Solla
1adf5f3d73
Merge pull request #432 from fsolla/B166-largura-padrao-do-chat-sollinha-com-teto-no-desktop
feat(B166): largura padrão do chat Sollinha com teto de 360 px e resize livre persistido
2026-08-08 16:24:30 -03:00
Francisco Solla
441c8d1156 refactor(B166): endurece e2e (poll determinístico) e clamp exato após revisão 2026-08-08 16:04:54 -03:00
Francisco Solla
c399b918c3 feat(worktree): comando plan — worktree dedicado ao /plan-issue
Novo subcomando `pnpm worktree plan [--go] [--no-migrate]` cria/reutiliza o
worktree de planejamento no branch fixo `plans/plan-issue`
(`PLAN_WORKTREE_BRANCH`), sem vínculo com a fila de claim: um `next` posterior
da próxima Issue nunca colide em branch nem em slot (slot derivado por hash do
branch, bumpado entre worktrees vivos). Mesmo provisionamento isolado do
`next` — porta 3100+slot, bancos teqo_wt<slot>/teqo_wt<slot>_test, migrations.

- scripts/worktree.mjs: cmdPlan + provision() com purpose/issue opcionais
- scripts/lib/worktree.mjs: constante PLAN_WORKTREE_BRANCH
- tests/unit/worktree.unit.spec.ts: derivação de slot do plano
- docs: .opencode/commands/worktree.md, shell, skills, AGENTS.md, changelog
2026-08-08 16:00:27 -03:00
Francisco Solla
2ca1afa6a7 feat(B166): largura padrão do chat Sollinha com teto de 360 px e resize livre persistido 2026-08-08 15:32:07 -03:00
Francisco Solla
ec0b109b5a
Merge pull request #430 from fsolla/B168-desfazer-encadeamento-automatico-das-acoes-rapidas-do-w
B168 — Desfazer o encadeamento automático das ações rápidas do wizard
2026-08-08 14:30:27 -03:00
fsolla
7faf3ed27f feat(campanha): B168 — desfazer encadeamento automático das ações rápidas do wizard
Ações de ajuste (votos, tendência, atualização, liderança) agora retornam à
origem ao salvar (toast + from allowlist B110 / Início), sem avançar sozinhas
para outro wizard. Removidos fila de encadeamento (wizardActionChain.ts),
param entry, skip/Pular (chrome mobile sempre X) e prefill de nota sem
produtor. Liderança: botão pós-save 'Continuar' → 'Concluir' (origem).
Sem migration.
2026-08-08 14:10:47 -03:00
Francisco Solla
bd06018bde B164: barra de navegação inferior no mobile para staff
Mobile bottom nav (staff only): 5 items (Início, Municípios, Atualizações,
Agenda, Mais) with Drawer overflow for secondary destinations. Leader and
desktop remain sidebar-only. FAB + scroll padding adjusted for safe-area.

Resolves #400
2026-08-08 12:57:05 -03:00
Francisco Solla
9447686edf
Merge pull request #421 from fsolla/worktree-envs-isoladas
feat(scripts): worktree next provisiona ambientes isolados (porta + bancos + migrations)
2026-08-08 11:51:46 -03:00
Francisco Solla
25773d0296
feat(C16): add calendar feed sync by filter with Google Calendar import link (#419)
- Add calendarFeed collection with secret slug credential
- Add iCal feed endpoint at /campanha/agenda/ical/[secret] (unauthenticated)
- Add server actions for creating and revoking calendar feeds
- Add CalendarFeedButton UI component in agenda filters bar
- Add unit tests for iCal generation
- Fail-closed access: feed re-evaluates creator's municipality scope on each read
- Leader lockdown preserved; no PII in feed output
2026-08-08 14:47:17 +00:00
Francisco Solla
8125334401 feat(worktree): ambientes isolados por worktree — porta própria, bancos próprios, migrations no provision
pnpm worktree next [--go] [--no-migrate] agora provisiona um ambiente
determinístico por worktree: slot = parte numérica do código da Issue
(C15→15; hash sem dígitos; bump em colisão entre worktrees vivos), dev
server em 3100+slot, bancos teqo_wt<slot> / teqo_wt<slot>_test no
container compartilhado (-p teqo) com a cadeia de migrations aplicada;
escreve .env.local + .env.test.local (gitignored, marcador gerado; envs
manuais nunca sobrescritos) e copia PAYLOAD_SECRET/Blob/VAPID do main.
kill remove o worktree e dropa os bancos gerados (WITH FORCE, best-effort).

Suites leem .env.test.local depois de .env.test (vitest.setup /
playwright / db-doctor / gate-ci espelham o layering) e
assertTestDatabase aceita teqo_test ou teqo_<worktree>_test em localhost
só (fail-closed preservado). db:start/db:stop fixam -p teqo (um
container de qualquer diretório); max_connections 200→400. Sem Docker
(Cursor Cloud): degrada ao compartilhado com aviso.

Derivação pura em scripts/lib/worktree-env.mjs (unit-tested); fluxo
validado end-to-end: provision → migrate → 598/598 int no banco do
worktree → kill sem resíduo.
2026-08-08 11:33:07 -03:00
Francisco Solla
c5001b5005
Merge pull request #410 from fsolla/C87-atualizacao-unificada-texto-polaridade-urgente
feat(campaign): C87 — atualização unificada (texto + polaridade + urgente)
2026-08-07 07:20:28 -03:00
Francisco Solla
212a1f2423 feat(campaign): C87 — atualização unificada (texto + polaridade + urgente)
Substitui o modelo de MunicipalityUpdate (kind/semanal/urgente/nota/sinal + signalType)
por um modelo unificado: body obrigatório + polarity (boa/neutra/ruim) + urgent + adversarySignal.

- Schema: polarity enum obrigatório, urgent boolean default false, adversarySignal boolean
- Wizard: novo WizardUpdateBodyStep com campo de texto + polaridade + urgente
- Lista: controle unificado de atualização com polaridade inline
- Feed: exibe polaridade como badge colorido + indicador de urgência
- V2 status strip: atualização rápida de polaridade
- E11: queries atualizadas para polarity + adversarySignal
- Migração: 20260807_030000_unify_municipality_update (backfill polarity=neutra, urgent de kind, adversarySignal de kind+signalType)
- Testes: 18/18 integração C87, 593/593 total, 1399/1399 unitários
- Fix: testes de state deputy usavam nomes fixos que colidiam entre execuções
2026-08-07 05:04:41 -03:00
Francisco Solla
55178247c8
Merge pull request #409 from fsolla/C15-fullcalendar-em-campanha-agenda
feat(campanha): C15 — FullCalendar em /campanha/agenda
2026-08-07 04:56:52 -03:00
Francisco Solla
59983b33f7 feat(campanha): C15 — FullCalendar em /campanha/agenda
Issue #390 — entrada operacional staff com FullCalendar v7 route-local
(semana/mês/lista), filtros compartilháveis por município/deputado/tag,
criação pelo slot, detalhe por URL e remarcação drag/resize sob RBAC,
transação e lock por atividade.

- /campanha/atividades e giros permanecem como lista/fluxos legados
- Sem migration/collection nova; reutiliza activity de C14
- Fuso Bahia, range exclusivo, erro/revert, loading/empty testados
- Leader negado, advisor limitado pelo access atual
- E2E cobre filtros, drag, mobile 390x844 e slot prefill
- Int specs cobrem RBAC, concorrência e defense-in-depth

Impl: docs/plans/fullcalendar-agenda-campanha-impl.md
2026-08-07 04:37:36 -03:00
Francisco Solla
b61f1ec36a style(tests): prettier-format worktree unit spec 2026-08-07 01:27:43 -03:00
Francisco Solla
1c0648d59a feat(scripts): deterministic worktree + issue CLI (next --go, kill, all) 2026-08-07 01:27:22 -03:00
Francisco Ignácio Fontoura Solla
033c270a89 feat(campanha): B163 — editar contatos das dobradinhas 2026-08-06 16:52:55 -03:00
Francisco Ignácio Fontoura Solla
adc29c0379 feat(campaign): C14 remodel activity for agenda paradigm (#389)
- Replace kind enum with free-form tags (text[] hasMany)
- Remove origin and deadline fields
- Status: remove rascunho/planejado, keep confirmado/realizado/cancelado
- startAt required for user-facing creates (optional for tour drafts)
- Add deputyPresent time gate (only coordinator/candidate can reschedule)
- UI: calendar-like form with tag input, no origin/deadline, no draft status
- E13 giros: tourStopRoleActivityKind → tourStopRoleActivityTags
- Migration: kind→tags with label mapping, status remap, enum recreation
- Activity texts table creation (was missing from migration history)

Closes #389
2026-08-06 05:20:05 -03:00
Cursor Agent
5f97b6a21c
feat(ai): add buildCampaignLinks tool for Sollinha navigation (B162)
Introduce a closed-catalog URL builder that delegates to existing list/detail
href helpers, with role-based guards for leader lockdown and unrestricted-only
assessor routes. Register the tool in the AI chat and teach the system prompt
when to offer markdown links after resolving entity ids/slugs.

Closes #383
2026-08-06 00:22:11 +00:00
Francisco Solla
a36b0b2625 simplify: safe fallback for browsers without container queries, trim test overhead 2026-08-05 12:28:17 -03:00
Francisco Solla
150bea2621 feat: make municipality columns follow content width 2026-08-05 11:35:20 -03:00
Francisco Solla
423c61d40b feat: distinguish municipality column defaults from explicit visibility 2026-08-05 10:49:58 -03:00
Francisco Ignácio Fontoura Solla
9fa701633e feat(campaign): unify municipality relation editors 2026-08-05 02:01:40 -03:00
Francisco Ignácio Fontoura Solla
896d957497 chore: migrate .cursor/ references to .agents/ folder
Replace all project-local .cursor/ path references with .agents/ across
rules, skills, environment config, hooks, and ignore files. Keep Cursor
editor paths (~/.cursor/, window.cursor, CURSOR_API_KEY) and vendored
Impeccable editor-hook paths unchanged.
2026-08-05 00:09:09 -03:00
Francisco Solla
4b79f00125
Merge pull request #373 from fsolla/feature/ai-chat-sollinha
feat: AI chat 'Sollinha' — campaign data assistant (v1 read-only)
2026-08-04 06:26:48 -03:00
Francisco Ignácio Fontoura Solla
f9cd1df7b2 feat: AI chat 'Sollinha' — campaign data assistant (v1 read-only)
- Streaming AI endpoint at /campanha/api/ai-chat with DeepSeek V4 Flash
- 8 tools: calculate, getMunicipalityVotes, getTopDeputies, getDobradinhas,
  getMunicipalityOverview, getLeaderships, getOrganizations, searchEntities
- In-memory rate limiter (50 msgs / 15 min per user)
- Desktop: resizable right sidebar via react-resizable-panels
- Mobile: full-screen Drawer opened by FAB (2/3 size of quick-actions FAB)
- Markdown rendering with react-markdown + remark-gfm (tables, bold, lists)
- Session-bound context (clean slate each open)
- Auth via existing campaign-token cookie, tools respect user RBAC
- Architecture doc: docs/plans/ai-chat-sollinha.md
- E2E tests: campaignAiChat.e2e.spec.ts (mocked endpoint)
2026-08-04 06:17:31 -03:00
Francisco Ignácio Fontoura Solla
6b8864debb B156 — assessores por dobradinha (campo, coluna e seção; RelationOptionCell extraído) 2026-08-04 04:57:15 -03:00
Francisco Ignácio Fontoura Solla
9780bb23df style: format campaignComponents unit spec 2026-08-04 02:53:44 -03:00
Francisco Ignácio Fontoura Solla
086a9c8ee0 B157 — coluna de dobradinhas na lista de municípios (+ criação inline) 2026-08-04 02:53:43 -03:00
Francisco Ignácio Fontoura Solla
596085c977 feat(campanha): B155 — coluna de lideranças na lista de municípios
Espelho da coluna de Assessores: chips com nome do contato, popover/sheet
com Command + delta otimista e criação inline (Nome + Celular). Sem
migration — leitura reversa de leadership.municipalities no bundle e
escrita reusando as actions B34/wizard; rota POST
/campanha/municipios/leaderships via campaignJsonMutationRoute.
2026-08-04 01:49:55 -03:00
Francisco Ignácio Fontoura Solla
b74268714c C86+ — simplify: JSDoc do contrato home, testes de borda e nota no impl plan
Fixes do /simplify (3 revisores read-only): JSDoc de normalizePathname/isListPath
documentando o invariante (homes nunca são '/' nem terminam em slash), teste de
multi-slash e inputs degenerados, comentário de isCampaignHomePath encurtado.
Sem mudança de comportamento.
2026-08-04 01:17:53 -03:00
Francisco Ignácio Fontoura Solla
4a90fb19ec C86+ — pathname de lista das ações rápidas em fonte única (isListPath)
Extrai isListPath + normalizePathname exportados em campaignQuickActionPaths.ts
e migra os 9 wrappers (7 públicos + 2 privados) + a 3a cópia de normalize em
campaignPageChrome.ts. Refactor estrutural sem mudança de comportamento;
derivação de regex de detalhe numérico adiada (gatilho: 3o vertical [id]).
Inclui docs/plans/escala-dry-pos-acoes-rapidas-impl.md e o fix de format
pré-existente de docs/plans/liderancas-coluna-municipios.md (bloqueava format:check).
2026-08-04 01:14:35 -03:00
Francisco Ignácio Fontoura Solla
26b2205c3a B154 — criar assessor inline no popover da lista de municípios
Closes #352

- POST /campanha/municipios/advisors aceita body de criação (name) na union;
  createMunicipalityAdvisorRecord cria campaignUser advisor (stub
  <slug>@criado.invalid, senha aleatória) e atribui ao município na mesma
  transação (cap 10 com rollback, lock municipality-advisors:{id})
- MunicipalityAdvisorCreateProvider compartilha o assessor criado entre linhas;
  opção "+ Criar assessor" e chip otimista com id temporário no control
- advisorNameSchema unificado em primitives.ts; predicado de placeholder cobre
  @criado.invalid (reset de senha e célula de e-mail tratam stubs inline)
- Destrava o spec e2e de municipios (falhas pré-existentes em main): key
  warning do fragment trailing de MunicipalityFilters; settle-wait da animação
  do Drawer no teste B136
2026-08-03 22:46:06 -03:00
Francisco Ignácio Fontoura Solla
cf09cb548d format: Prettier 2026-08-03 21:06:52 -03:00
Francisco Ignácio Fontoura Solla
d200a1b012 B86 — ações rápidas — apoiadores (staff): catálogo no FAB/overlay 2026-08-03 21:03:13 -03:00
Cursor Agent
fb75f7235b
fix(P5-P1): schedule campaign notification push after txn commit
queueMicrotask under req.transactionID raced withPayloadTransaction
commit. Register after-commit callbacks, soft-fail missing notification
rows on send, and pin the schedule with a convention unit spec.
2026-08-03 04:40:38 +00:00
Cursor Agent
30a5a8a5e3
B151 — FAB secundário no detalhe município v2 (#334)
- Catálogo v2 de ações rápidas: dossiê, eleições, nova liderança, giro
- Integra no chassis B126 via registry + context sync na rota v2
- Omitido registrar sinal (já na faixa de status B147)

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 01:03:10 +00:00
Francisco Solla
2414001606
Merge pull request #343 from fsolla/cursor/pool-i333-5914f625-bd1e
B150 — Município v2 bloco Agora (encaminhamento + sugestão + visita)
2026-08-02 21:53:38 -03:00
Francisco Solla
d134dd0462
Merge pull request #346 from fsolla/cursor/pool-i315-94cacb95-bf11
B145 — Detalhe de entidade — título no header (sem hero espaçoso)
2026-08-02 21:52:20 -03:00
Francisco Solla
7753470a3b
Merge pull request #345 from fsolla/cursor/pool-i296-f91a8b70-0be4
OPS18 — Promote plan-issue dual (agente + Action no merge)
2026-08-02 21:45:56 -03:00
Cursor Agent
5696aef9ed
B145 — move entity identity to shell header on detail pages
- Detail routes return null from campaignPageChrome catalog (override per page)
- SetCampaignPageChrome + generateMetadata on 8 entity detail routes
- Remove body hero (h1, Voltar links, duplicate identity chips)
- Keep operational badges/chips in page body
- Update zone map e2e to assert shell chrome title slot

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:29:48 +00:00
Cursor Agent
debfd37919
style: prettier B150 files 2026-08-03 00:28:54 +00:00
Cursor Agent
6955cccc6b
B150 — Município v2 bloco Agora (encaminhamento + sugestão + visita)
- Loader e seção Agora na rota /campanha/municipio/[slug]/v2
- Encaminhamento editável com auto-save via POST /municipios/next-steps
- Até 2 sugestões E11 com resolve/dispensa existente; silêncio explícito
- Visita E13 condensada em uma linha + link ao compositor de giros
- Revalidação v2 ao resolver sugestão; helper compartilhado de empty state E11

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:28:47 +00:00
Francisco Solla
deefe566b0
Merge pull request #341 from fsolla/cursor/pool-i332-0d1ca390-b998
B149 — Município v2 — rede (lista edit-in-place)
2026-08-02 21:24:44 -03:00
Cursor Agent
7665b1062c
fix(ops): OPS18 simplify — Related regex, already-ready, job gate
Aperta parseRelatedIssueNumbers (anti hyphen), canPromotePlanIssue
already-ready, workflow só com Related # no body + PR_BODY, soft-fail
por Issue no Action script.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:22:55 +00:00
Francisco Solla
90bbfeb2da
Merge pull request #339 from fsolla/cursor/pool-i302-2f4dd995-03ba
B136 — Início / FAB — grade 3 colunas × 2 linhas (corrigir orientação)
2026-08-02 21:20:55 -03:00
Cursor Agent
b7f4e16c0f
feat(ops): OPS18 — Action promote Related #N no merge
Safety net dual com agent:ready: no merge em main, workflow lê Related #N
e promove Issues ainda aguardando plano (canPromotePlanIssue), soft-skip
idempotente. Helper parseRelatedIssueNumbers + pins unitários.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:20:48 +00:00
Francisco Solla
1266b47b0e
Merge pull request #336 from fsolla/cursor/pool-i292-298f3db9-b2f7
OPS17 — Ciclo de vida plan-issue — colaboração fechada antes de claim
2026-08-02 21:17:03 -03:00
Francisco Solla
43dcd8da9b
Merge pull request #325 from fsolla/cursor/pool-i306-4a43a35f-79fc
B138 — Atividades: presets e busca na omnibox; remover abas
2026-08-02 21:14:02 -03:00
Cursor Agent
7d89356710
B149 — Município v2 rede: lista edit-in-place com top-N lideranças
- Seção Rede na rota v2 com tabela/lista densa e CTAs Ver todas / Nova
- Células autosave (B9) para declarado e estimado via JSON routes
- Loader + view puro com ordenação por estimado/declarado
- Revalidate v2 em writes de pledge; unit tests de merge/sort

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:10:30 +00:00
Cursor Agent
d2cc1d2693
feat(ops): OPS17 — plan-issue blocked até plano em main
Register com --plan nasce blocked (não claimável); pnpm agent:ready
promove após o plano em main. Gate da skill plan-issue endurecido.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:06:14 +00:00
Cursor Agent
0d124b47cb
fix: restore searchOnlyListOmnibox exports from main (CI merge) 2026-08-03 00:01:48 +00:00
Cursor Agent
a2e952b3a2
fix(B136): resolve listOmniboxB128 merge conflict markers for CI 2026-08-02 23:53:33 +00:00
Cursor Agent
a2553e1f03
merge main into B147; keep optional sinal body for v2 status
Align with B134 engagement write path; preserve B147 sinal empty-motivo
(collection + zod) required by the status-strip confirm rite.
2026-08-02 23:48:29 +00:00
Francisco Solla
e3a54be217
Merge pull request #337 from fsolla/cursor/pool-i313-f30f822f-8e1d
B144 — Sidebar — entrada para Organizações
2026-08-02 20:38:36 -03:00
Cursor Agent
764343da8e
test(B128): cover search-only omnibox exports for knip graph 2026-08-02 23:36:37 +00:00
Cursor Agent
2f2c8563a5
B136 — corrigir grade 3×2 no Início e FAB overlay
- layout=grid no botão com w-full/min-w-0 sem box-content no mobile
- li min-w-0, gap-2, data-layout=grid-3 no strip
- e2e com geometria (boundingBox) em viewport 390
- impl plan docs/plans/inicio-acoes-grade-3cols-2rows-impl.md

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:36:28 +00:00
Cursor Agent
6b8aa8b6cf
fix(campanha): B147 simplify — sinal empty body + cold select
Allow empty motivo on sinal through collection beforeValidate; distinguish
warm-typeless vs cold in the v2 signal select; reuse list signal form action.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:33:46 +00:00
Cursor Agent
c66ffd598c
fix(campanha): map municipio v2 route in e2e affected manifest
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:30:51 +00:00
Cursor Agent
0aa47f8673
feat(campanha): B147 município v2 shell + faixa de status
Rota paralela /campanha/municipio/<slug>/v2 com selects de nível, tendência e
sinal (modal de motivo opcional), classe territorial e agregado. Soft-dep B134:
motivo opcional no movimento de nível e remoção do campo “voltar atrás” do
write path/lista.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:29:05 +00:00
Cursor Agent
4ea2085239
B138 — simplify: min-length e normalização da busca
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:26:48 +00:00
Cursor Agent
f0a7758eb3
B138 — atividades: presets e busca na omnibox; remove abas
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:26:48 +00:00
Cursor Agent
7fe42a5fa5
test(omnibox): cover search-only helpers for knip graph (#313)
Knip could not see exports only referenced from client filter components;
extend B128 unit spec so CI knip stays green.
2026-08-02 23:25:50 +00:00
Francisco Solla
6539d0cc55
Merge pull request #329 from fsolla/cursor/pool-i288-97b0de1d-f857
B134 — Motivo opcional (tendência + nível); remover voltar atrás do DB
2026-08-02 20:25:23 -03:00
Francisco Solla
466a34525d
Merge pull request #328 from fsolla/cursor/pool-i317-1bee052c-c52f
B146 — FAB overlay idle on open; close/handle chrome
2026-08-02 20:23:54 -03:00
Francisco Solla
8a60b6f0f7
Merge pull request #326 from fsolla/cursor/pool-i310-73006baf-d8e9
B142 — Assessores — filtrar por município da carteira na omnibox
2026-08-02 20:22:16 -03:00
Francisco Solla
56b96198cb
Merge pull request #324 from fsolla/cursor/pool-i309-84ab81f4-8444
B141 — Apoiadores — filtro por fonte na omnibox
2026-08-02 20:20:40 -03:00
Francisco Solla
66c84a9f6b
Merge pull request #321 from fsolla/cursor/pool-i289-de3861a0-5cd2
B135 — Wizard Voltar = passo imediatamente anterior na cadeia
2026-08-02 20:19:52 -03:00
Francisco Solla
c90c7d3722
Merge pull request #323 from fsolla/cursor/pool-i311-f0d9725a-dfcb
B143 — Lideranças — filtrar por organização e dobradinha na omnibox
2026-08-02 20:19:41 -03:00