diff --git a/docs/plans/escala-dry-pos-visitados-recentemente.md b/docs/plans/escala-dry-pos-visitados-recentemente.md
new file mode 100644
index 00000000..a6468df3
--- /dev/null
+++ b/docs/plans/escala-dry-pos-visitados-recentemente.md
@@ -0,0 +1,100 @@
+# Escala e DRY pós-visitados recentemente
+
+Status: registrado no roadmap (fases pendentes)
+Atualizado em: 2026-07-19
+Item do roadmap: [docs/roadmap.md](../roadmap.md) (VR+, fill-in de engenharia pós-MVP)
+Responsável: —
+
+## Contexto
+
+O fill-in **Visitados recentemente** ([visitados-recentemente.md](visitados-recentemente.md)) entregou o MVP client-side: `recentVisits.ts` + `RecentVisitTracker` + `RecentlyVisited` no dashboard `/campanha`, integração em detalhe/listagem de núcleos, `buildNucleusListVisitLabel` / `buildNucleusListVisitHref`, labels de cobertura/estimativa compartilhados com `NucleusFilters`, e limpeza no logout (`CampaignSidebar`). Três passagens `/simplify` (2026-07-19) aplicaram cleanup pontual; os revisores (quality / performance / reuse) deixaram débitos **maiores que cleanup** — registrados aqui.
+
+**Já resolvido no simplify (não reabrir):** `formatRelativeAge` extraído para `src/utilities/formatRelativeAge.ts` (evita puxar `campaignTime.ts` no client); prop `now` do servidor; remoção de `RECENT_VISITS_CHANGED_EVENT` (morto — painel e tracker não coexistem na mesma rota); `nucleusListCoverageLabels` / `nucleusListEstimateLabels` compartilhados; `buildNucleusListVisitHref` sem `page` (dedup de paginação); `listVisitHref` lazy no branch com filtro.
+
+## Objetivos
+
+- Painel reflete visitas recentes após navegação back/forward (bfcache) e escrita em outra aba do mesmo origin.
+- Um único helper de tempo relativo no dashboard (substituir `lastUpdateLabel` / `relativeDateLabel` locais onde fizer sentido).
+- Linha de lista do dashboard reutiliza shell compartilhado (`QueueList` ↔ `RecentlyVisited`) sem duplicar markup.
+- `RecentlyVisited` montado uma vez no wrapper do dashboard, respeitando `max-width` por role.
+- Guardrails: sem collection/migration/Consent; histórico continua client-only; comportamento de dwell/dedup inalterado salvo bugs encontrados.
+
+## Decisões travadas
+
+- **Um plano VR+, quatro fases ordenadas.** Mesmo racional de O0+/B5/C8: um registro no roadmap, PRs por fase. Ordem: refresh confiável → DRY tempo relativo → DRY row shell → DRY layout dashboard.
+- **Dependência suave do fill-in Visitados recentemente ✓.** Não bloqueia uso do MVP; melhora UX em back-navigation e consistência visual.
+- **Sem sync servidor.** Histórico multi-dispositivo, poda reativa de acesso e histórico de planos/apoiadores permanecem fora de escopo (exigiriam modelo + `Consent`).
+- **Cortável:** Fases 3–4 (row shell + layout wrapper) até o dashboard ganhar mais painéis do mesmo padrão; Fase 2 (relative time) é barata e reduz drift — preferir se houver folga.
+- **i18n e naming** (AGENTS.md): identificadores em inglês (`useRecentVisits`, `CampaignLinkListRow`, `formatLastUpdateLabel`), strings visíveis em pt-BR.
+
+## Questões em aberto
+
+- **`pageshow` + `storage` vs reintroduzir evento custom?** O evento foi removido porque tracker e painel não montam juntos; bfcache e multi-tab são os casos reais restantes. **Recomendação:** `pageshow` (`event.persisted`) + `window.addEventListener('storage', …)` filtrando `STORAGE_KEY` — sem event bus custom.
+- **Unificar `floor` (dias) vs `round` (minutos/horas)?** `lastUpdateLabel` usa dias inteiros; `formatRelativeAge` arredonda minutos/horas/dias. **Recomendação:** exportar variantes explícitas em `formatRelativeAge.ts` (`formatRelativeAgeDays`, `formatRelativeAge`) em vez de forçar um único comportamento.
+- **Extrair row shell agora?** Só dois consumidores (`QueueList`, `RecentlyVisited`) com diferenças (ícone leading vs `ArrowRight`, prefixo "Última atualização"). **Recomendação:** extrair quando um terceiro painel de atalhos aparecer, ou na Fase 3 se o drift visual incomodar produto.
+- **`next/dynamic` no painel?** **Recomendação:** só se Lighthouse mostrar o chunk como relevante no `/campanha`; o painel já retorna `null` quando vazio.
+
+## Abordagem proposta
+
+```mermaid
+flowchart TD
+ VR["Visitados recentemente MVP ✓"] --> F1
+ F1["Fase 1 — Refresh confiável
pageshow + storage listener"]
+ F1 --> F2["Fase 2 — DRY tempo relativo
formatRelativeAge variants"]
+ F2 --> F3["Fase 3 — CampaignLinkListRow
(QueueList + RecentlyVisited)"]
+ F3 --> F4["Fase 4 — Dashboard shell
(RecentlyVisited hoisted)"]
+```
+
+### Fase 1 — Refresh confiável do painel
+
+- **`RecentlyVisited`** (`src/components/campaign/RecentlyVisited.tsx`): no `useEffect` de mount, registrar `pageshow` (refresh quando `event.persisted`) e `storage` (quando `e.key === STORAGE_KEY` ou `e.key === null` para `clear()`).
+- Opcional: extrair `useRecentVisits()` (`useState` + listeners + `listRecentVisits`) se Fase 1 crescer — hoje um hook de ~15 linhas é suficiente.
+
+### Fase 2 — DRY de tempo relativo no dashboard
+
+- **`formatRelativeAge.ts`**: adicionar `formatRelativeAgeInDays(timestampMs, nowMs)` (floor, como `lastUpdateLabel`) além do bucketing atual.
+- **`CampaignDashboard.tsx`**: migrar `lastUpdateLabel` para helper compartilhado (manter prefixo "Última atualização" no call site).
+- **`NucleusListOverview.tsx`**: migrar `relativeDateLabel` para o mesmo helper (dia-only).
+- **`NucleusUpdateFeed.tsx`**: substituir `new Intl.RelativeTimeFormat` por instância do módulo compartilhado (hoje aloca formatter a cada call).
+
+### Fase 3 — DRY de linha de lista
+
+- Extrair **`CampaignLinkListRow`** (ex. `src/components/campaign/CampaignLinkListRow.tsx`): props `href`, `title`, `subtitle?`, `leadingIcon?`, `trailingIcon?` (`ArrowRightIcon` default para filas).
+- Refatorar **`QueueList`** em `CampaignDashboard.tsx` e **`RecentlyVisited`** para usar o componente; alinhar classes (`truncate` no subtítulo, `min-h-11`).
+
+### Fase 4 — Shell do dashboard por role
+
+- Introduzir wrapper **`CampaignDashboardShell`** com prop `maxWidthClass` (`max-w-screen-2xl` / `max-w-5xl` / `max-w-4xl`) renderizando header slot + **``** + children.
+- `GeneralDashboard` / `CoordinatorDashboard` / `LeadershipDashboard` passam a compor o shell em vez de repetir o painel.
+
+### Fase 5 — Micro-perf (opcional, cortável)
+
+- **`next/dynamic`** com `ssr: false` para `RecentlyVisited` em `CampaignDashboard.tsx` se o bundle do dashboard for gargalo.
+- **`recordRecentVisit`**: leitura interna sem re-validação completa no write path (`_readRaw`) — só se profiling mostrar custo (improvável com `MAX_ENTRIES = 8`).
+- **`nucleos/page.tsx`**: deduplicar `resolveNucleusListUrl` (pré-existente, fora do core visitados) — registrar junto com A7/C10 se virar padrão de lista.
+
+## Dependências
+
+- **Visitados recentemente MVP ✓** (fill-in entregue em branch local; merge pendente).
+- Reusa `formatRelativeAge.ts`, `recentVisits.ts`, `CampaignDashboard.tsx`, `NucleusListOverview.tsx`, `NucleusUpdateFeed.tsx`.
+- Sem migration, sem collection, sem `Consent`.
+
+## Não escopo
+
+- Sync multi-dispositivo / histórico no servidor → exigiria collection + `Consent` (permanece no [visitados-recentemente.md](visitados-recentemente.md)).
+- Histórico de planos/apoiadores/outras rotas → item separado se produto pedir.
+- Poda reativa ao perder acesso (best-effort no MVP) → só se virar confusão recorrente.
+- Telemetria/analytics de navegação.
+- `buildNucleusDetailHref` — inline `/campanha/nucleos/${slug}` é o padrão do codebase hoje.
+
+## Referências
+
+- [visitados-recentemente.md](visitados-recentemente.md) — MVP entregue
+- [escala-dry-pos-onda0.md](escala-dry-pos-onda0.md) — precedente de registro pós-`/simplify`
+- `src/utilities/recentVisits.ts`, `src/utilities/formatRelativeAge.ts`
+- `src/components/campaign/RecentlyVisited.tsx`, `RecentVisitTracker.tsx`
+- `src/components/campaign/CampaignDashboard.tsx` (`QueueList`, `lastUpdateLabel`)
+- `src/components/campaign/NucleusListOverview.tsx`, `NucleusUpdateFeed.tsx`
+- `src/utilities/nucleusUi.ts` (`buildNucleusListVisitLabel`, `buildNucleusListVisitHref`)
+- `docs/roadmap.md` (fill-ins VR+)
+- AGENTS.md — naming, sem migration para este item
diff --git a/docs/plans/visitados-recentemente.md b/docs/plans/visitados-recentemente.md
index 9270ef15..54a0e04d 100644
--- a/docs/plans/visitados-recentemente.md
+++ b/docs/plans/visitados-recentemente.md
@@ -1,7 +1,7 @@
# Visitados recentemente
-Status: rascunho
-Atualizado em: 2026-07-17
+Status: implementado
+Atualizado em: 2026-07-19
Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos")
Responsável: —
@@ -43,14 +43,16 @@ A `window.history` **não expõe** a lista de entradas (URLs) da stack por motiv
## Questões em aberto
-- **Registrar listagem só com filtro ativo?** **Recomendação:** sim — a listagem sem filtro é o destino padrão do menu e não agrega. Definir com produto se faz sentido registrar também buscas (`q=`) isoladas.
-- **Valor exato do dwell.** Default proposto: 2000 ms. Muito curto registra estados intermediários; muito longo perde visitas rápidas legítimas (ex.: abrir um núcleo só para checar um dado). **Recomendação:** 2000 ms como ponto de partida, ajustar com uso. Definir com produto.
-- **Formato da label do filtro.** Ex.: `Núcleos · Região X`, `Núcleos · Cidade Y · Com estimativa pendente`, `Núcleos · Busca "solla"`. **Recomendação:** label composta curta a partir do `NucleusListState` (território + cobertura + busca), truncada. Definir os rótulos exatos com produto.
-- **Onde mais mostrar, além do dashboard?** **Recomendação:** só no dashboard `/campanha` neste ciclo (é o "Início", landing natural). Repetir no topo da listagem de núcleos é opcional e pode poluir — deixar para depois.
-- **Limpeza de entradas obsoletas.** Hoje best-effort (link existe, destino pode negar). **Recomendação:** não limpar reativamente neste ciclo; o usuário pode limpar manualmente via botão "Limpar" no painel. Se virar confusão, adicionar poda ao clicar em entrada que resulta em acesso negado.
-- **Dispositivo compartilhado.** `localStorage` é por origem; em dispositivo compartilhado, um usuário vê histórico de outro (mesma origem). Não há PII de terceiros, mas nomes de núcleo são internos da campanha. **Recomendação:** aceitar (mesma superfície de qualquer app web nesta origem); se produto exigir isolamento, limpar ao `logoutCampaign` (já é client-side e barato).
-- **Limpar ao deslogar?** **Recomendação:** sim — chamar `clearRecentVisits()` em `logoutCampaign` para não vazar histórico local na troca de usuário em dispositivo compartilhado. Confirmar com produto.
-- **PWA/offline.** O painel lê `localStorage`, então funciona offline (depende do item PWA do roadmap estar em pé para o modo offline; sem PWA, funciona online normalmente). Sem dependência dura neste plano.
+Resolvidas na implementação (2026-07-19):
+
+- **Registrar listagem só com filtro ativo?** Sim — `buildNucleusListVisitLabel` retorna `null` sem filtros; `q=` conta; só `page` não conta.
+- **Valor exato do dwell.** 2000 ms (`RECORD_DWELL_MS` em `recentVisits.ts`).
+- **Formato da label do filtro.** `Núcleos · {região} · {município} · Zona N · {cobertura} · {estimativa} · Busca "…"`, truncada em 80 caracteres.
+- **Onde mais mostrar, além do dashboard?** Só no dashboard `/campanha` (três variantes por role).
+- **Limpeza de entradas obsoletas.** Best-effort; botão "Limpar" no painel.
+- **Dispositivo compartilhado.** Aceito; histórico limpo no logout.
+- **Limpar ao deslogar?** Sim — `clearRecentVisits()` em `CampaignSidebar.handleLogout` (client-side, junto com `clearCampaignPwaCaches`).
+- **PWA/offline.** Painel lê `localStorage`; funciona offline quando o PWA estiver ativo.
## Abordagem proposta
@@ -100,7 +102,7 @@ Componentes:
- **Integração no dashboard** (`src/app/(campaign)/campanha/(app)/page.tsx` + `CampaignDashboard.tsx`): renderizar `` como um painel (recomendação: no topo, antes dos agregados, como atalho de "voltar aonde estava"; confirmar com produto). Como é client-only, o servidor renderiza o placeholder vazio e o cliente preenche.
- **Integração no detalhe do núcleo** (`src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx`): renderizar `` (invisível).
- **Integração na listagem** (`src/app/(campaign)/campanha/(app)/nucleos/page.tsx`): computar `label = buildNucleusListVisitLabel(state)`; se `label` não for `null`, renderizar `` (invisível).
-- **Logout** (`src/app/(campaign)/campanha/actions/auth.ts`): chamar `clearRecentVisits()` em `logoutCampaign` (se confirmado) — protege dispositivo compartilhado.
+- **Logout** ([`src/components/campaign/CampaignSidebar.tsx`](src/components/campaign/CampaignSidebar.tsx)): `clearRecentVisits()` em `handleLogout`, junto com `clearCampaignPwaCaches()` — protege dispositivo compartilhado.
- **Sem migration, sem collection, sem server action de escrita.** Tudo é leitura no servidor + estado client-side.
## Dependências
@@ -116,12 +118,17 @@ Componentes:
- Registro de telemetria/analytics — este é histórico de navegação local do usuário, não rastreamento para a campanha.
- Substituir a navegação principal (sidebar/bottom nav) — o painel é atalho complementar.
+## Escala e DRY pós-MVP (VR+)
+
+Três passagens `/simplify` (2026-07-19) aplicaram cleanup pontual no MVP. Débitos **maiores que cleanup** — refresh confiável após bfcache/multi-tab, unificação de tempo relativo no dashboard, shell compartilhado de linha de lista (`QueueList` ↔ `RecentlyVisited`), hoisting do painel no wrapper do dashboard — foram registrados no plano de follow-up **[escala-dry-pos-visitados-recentemente.md](escala-dry-pos-visitados-recentemente.md)** (item **VR+** em [docs/roadmap.md](../roadmap.md), fill-ins).
+
## Referências
-- `docs/roadmap.md` (seção "Campanha → Próximos ciclos", item "Visitados recentemente")
+- `docs/roadmap.md` (fill-ins: Visitados recentemente ✓, VR+)
+- [escala-dry-pos-visitados-recentemente.md](escala-dry-pos-visitados-recentemente.md) — débitos pós-`/simplify`
- `src/app/(campaign)/campanha/(app)/page.tsx` e `src/components/campaign/CampaignDashboard.tsx` — onde o painel entra
- `src/app/(campaign)/campanha/(app)/nucleos/page.tsx` — listagem (registro de filtro)
- `src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx` — detalhe (registro de núcleo)
- `src/utilities/nucleusUi.ts` — `NucleusListState`, `resolveNucleusListUrl` (canonicalização do `href`/label)
-- `src/app/(campaign)/campanha/actions/auth.ts` — `logoutCampaign` (limpeza opcional)
+- `src/app/(campaign)/campanha/actions/auth.ts` — `logoutCampaign` (server action; limpeza do histórico fica no client)
- AGENTS.md — Campaign auth, naming conventions (identificadores em inglês, strings em pt-BR)
diff --git a/docs/roadmap.md b/docs/roadmap.md
index f7b24dee..82a08d47 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap — Teqo
-Atualizado em: 2026-07-19 (MVP + Ciclo 2 deployados; A2 entregue; A4 Baseline no produto + Gap vs 2022 implementado e mesclado em `main` (Fases 2–4 + simplify); A7 registrado — escala/DRY pós-A4 do `/simplify`; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6 Escala e DRY pós-C2 implementado e mesclado em `main` (Fases 1–5 + simplify); C7 parcial 2026-07-19 (FormData território, contadores lista, locks, contactSearchQuery) — débitos pós-simplify registrados no plano; C8 Escala e DRY pós-C6 implementado e mesclado em `main` (Fases 1–4 + duas passagens `/simplify`); C9 Escala e DRY pós-C8 implementado (Fases 1–4 + simplify); C10 registrado — escala/DRY pós-C9 do `/simplify`; B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`; **Trilha E registrada — E1–E5 mapa de projeção por município**, equiparar/superar as planilhas "Mapa de projeção de votos 2026" — [plano](plans/mapa-projecao-municipios.md); **Onda 0 MVP** — Consent/privacidade provisórios auto-provisionados (migrations) + `/privacidade`; hold de PII real até jurídico — [plano](plans/onda-0.md); **O0+ registrado** — escala/DRY pós-Onda 0 do `/simplify` — [plano](plans/escala-dry-pos-onda0.md))
+Atualizado em: 2026-07-19 (MVP + Ciclo 2 deployados; A2 entregue; A4 Baseline no produto + Gap vs 2022 implementado e mesclado em `main` (Fases 2–4 + simplify); A7 registrado — escala/DRY pós-A4 do `/simplify`; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6 Escala e DRY pós-C2 implementado e mesclado em `main` (Fases 1–5 + simplify); C7 parcial 2026-07-19 (FormData território, contadores lista, locks, contactSearchQuery) — débitos pós-simplify registrados no plano; C8 Escala e DRY pós-C6 implementado e mesclado em `main` (Fases 1–4 + duas passagens `/simplify`); C9 Escala e DRY pós-C8 implementado (Fases 1–4 + simplify); C10 registrado — escala/DRY pós-C9 do `/simplify`; B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`; **Trilha E registrada — E1–E5 mapa de projeção por município**, equiparar/superar as planilhas "Mapa de projeção de votos 2026" — [plano](plans/mapa-projecao-municipios.md); **Onda 0 MVP** — Consent/privacidade provisórios auto-provisionados (migrations) + `/privacidade`; hold de PII real até jurídico — [plano](plans/onda-0.md); **O0+ registrado** — escala/DRY pós-Onda 0 do `/simplify` — [plano](plans/escala-dry-pos-onda0.md); **Visitados recentemente MVP** implementado (fill-in, client-side `localStorage`) — [plano](plans/visitados-recentemente.md); **VR+ registrado** — escala/DRY pós-visitados recentemente do `/simplify` — [plano](plans/escala-dry-pos-visitados-recentemente.md))
Registro canônico no repositório dos planos futuros e débitos conhecidos. Status operacional do ciclo atual de Núcleos fica em [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc); este arquivo lista o que ainda é futuro ou bloqueador, **em ordem de execução**, com dependências e paralelismo explícitos.
@@ -113,12 +113,13 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig
| C9 Escala e DRY pós-C8 | — (sem design novo; reusa os de C2/C6/C8) | [escala-dry-pos-c8.md](plans/escala-dry-pos-c8.md) |
| C10 Escala e DRY pós-C9 | — (sem design novo; reusa os de C2/C6/C8/C9) | [escala-dry-pos-c9.md](plans/escala-dry-pos-c9.md) |
| O0+ Escala e DRY pós-Onda 0 | — (sem design novo; reusa `/privacidade` + padrão revalidate `posts`) | [escala-dry-pos-onda0.md](plans/escala-dry-pos-onda0.md) |
+| VR+ Escala e DRY pós-visitados | — (sem design novo; reusa dashboard + padrões de lista de núcleos) | [escala-dry-pos-visitados-recentemente.md](plans/escala-dry-pos-visitados-recentemente.md) |
| B5 Escala e DRY pós-B2 | — (sem design novo; reusa geometrias B2 + padrões de scripts CLI) | [escala-dry-pos-b2.md](plans/escala-dry-pos-b2.md) |
| D2 Notificações (sino) | `Notificacoes-PWA` (central de notificações) | [notifications.md](plans/notifications.md) |
| D1 PWA `/campanha` | `Notificacoes-PWA` (banner "Instale o app da campanha") | [pwa-campanha.md](plans/pwa-campanha.md) |
| C5 Operação dia D / GOTV | `Dia-D-GOTV` | sem plano ainda — o design é a primeira materialização do item (check-off de apoiadores por seção/zona, progresso "62% da base", filtro "Ainda não confirmados", estado "Sincronizado" offline-tolerante) |
-**Sem design nesta leva** (encomendar na próxima rodada ou desenhar na implementação): A6 dobradinha 2026, B2/B3/B4 mapa da Bahia, C4 demandas, E1–E4 mapa de projeção por município (seguir os padrões de `Baseline-Eleitoral-2022` e `Lista-Nucleos-Overview`), fill-ins (visitados recentemente, listas globais, reset de senha). O detalhe do plano de ação (tabs Visão geral/Tarefas/Atualizações) e a tela de opt-in de push também não vieram — os planos correspondentes indicam o padrão a seguir.
+**Sem design nesta leva** (encomendar na próxima rodada ou desenhar na implementação): A6 dobradinha 2026, B2/B3/B4 mapa da Bahia, C4 demandas, E1–E4 mapa de projeção por município (seguir os padrões de `Baseline-Eleitoral-2022` e `Lista-Nucleos-Overview`), fill-ins (listas globais, reset de senha). O detalhe do plano de ação (tabs Visão geral/Tarefas/Atualizações) e a tela de opt-in de push também não vieram — os planos correspondentes indicam o padrão a seguir.
### Grafo de dependências
@@ -218,7 +219,7 @@ flowchart TD
A7 -.mesmo loader do baseline.-> E2
```
-Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3, **O0+** (após engenharia Onda 0 ✓) e fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). **Entregues e mesclados em `main` em 2026-07-18/19** (✓ no grafo): A1, A2, A3, A4, B1, B2, C1, C2, C3, C6, C8, C9 e D1 — ver "Ciclo 2" e "Ciclo 2+" acima. C2 segue com produção bloqueada pelo lote jurídico (Onda 0: Consent keys `apoiador-cadastro` / `apoiador-intencao-voto` + aprovação). Destravados por eles: A5/A6 (após A4 ✓), A7 (após A4 ✓; preferencialmente antes ou com A5/B3/E2), E2 (após A4 ✓), B3 (B1 ✓ + B2 ✓; suave: A4 ✓ para coroplético e B5 F1 para lazy load), B5 (após B2; preferencialmente com B3), C4 (A1 + C3 prontos), C7 (após C3; suave: C6 F2 ✓), C10 (após C9 ✓; cortável se a base nominal permanecer pequena) e D2 push (D1 pronto; falta a chave jurídica). E4 destravará após E1+E3 (campos precisam existir antes do import).
+Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3, **O0+** (após engenharia Onda 0 ✓), **VR+** (após Visitados recentemente ✓) e fill-ins (listas globais, reset de senha, higiene PascalCase). **Entregues e mesclados em `main` em 2026-07-18/19** (✓ no grafo): A1, A2, A3, A4, B1, B2, C1, C2, C3, C6, C8, C9 e D1 — ver "Ciclo 2" e "Ciclo 2+" acima. C2 segue com produção bloqueada pelo lote jurídico (Onda 0: Consent keys `apoiador-cadastro` / `apoiador-intencao-voto` + aprovação). Destravados por eles: A5/A6 (após A4 ✓), A7 (após A4 ✓; preferencialmente antes ou com A5/B3/E2), E2 (após A4 ✓), B3 (B1 ✓ + B2 ✓; suave: A4 ✓ para coroplético e B5 F1 para lazy load), B5 (após B2; preferencialmente com B3), C4 (A1 + C3 prontos), C7 (após C3; suave: C6 F2 ✓), C10 (após C9 ✓; cortável se a base nominal permanecer pequena) e D2 push (D1 pronto; falta a chave jurídica). E4 destravará após E1+E3 (campos precisam existir antes do import).
### Sequência de execução por janela do calendário
@@ -273,12 +274,13 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): E1, E3, **O0
**Fill-ins (qualquer janela, quando houver folga; nenhum bloqueia nada):**
- **O0+ Escala e DRY pós-Onda 0** (revalidate globals, chaves Consent, testes SQL provision, DRY Lexical/layout) — engenharia pós-MVP; paralelizável com smoke Onda 0. → [detalhes](plans/escala-dry-pos-onda0.md)
-- **Visitados recentemente** (client-side, `localStorage`). → [detalhes](plans/visitados-recentemente.md)
+- **Visitados recentemente** ✓ (client-side, `localStorage`; MVP entregue 2026-07-19). → [detalhes](plans/visitados-recentemente.md)
+- **VR+ Escala e DRY pós-visitados recentemente** (refresh bfcache/storage, DRY tempo relativo, `CampaignLinkListRow`, shell do dashboard) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-visitados-recentemente.md)
- **Listas globais** (lideranças, atualizações, territórios no nível raiz). _(plano MVP; sem plano detalhado ainda)_
- **Reset de senha self-service + foto de perfil** (UX adiada do ciclo 1). _(plano MVP)_
- **Higiene de código:** varredura PascalCase dos componentes legados. _(notebook Núcleos)_
-**Cortes seguros se o prazo apertar** (nesta ordem): E5 Salvador por bairro (já registrado como futuro), B4 camada de zonas, B5 F2 (cache CLI — duplicação pequena), E4 import da planilha (os campos E1/E3 podem ser preenchidos à mão pela coordenação; o import só acelera), A7 F2–F3 (`cityCode` / Alert DRY — só se a geografia real dos núcleos permanecer 1–2 municípios), E2 série 2014/2018 (reduzível a só 2018 se o formato TSE de 2014 atrasar; a tendência vira 2 pontos), B3 mapa Leaflet (preferir levar B5 F1 lazy load junto se B3 entrar), C4 demandas, D2 push (mantendo o sino), C6 fases 3–5 (import bulk / preview token / KPI — só se a base nominal permanecer pequena), C7 fases 4–5 restantes (leituras por aba / short-circuit de hooks / feed O(n) — só se a agenda permanecer pequena; contadores + locks já entregues), C8 (escala/DRY pós-C6 — só se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas), C9 (escala/DRY pós-C8 — só se a base nominal permanecer pequena; Fases 3–4 DRY de forms podem entrar isoladas), C10 (escala/DRY pós-C9 — só se a base nominal permanecer pequena; Fase 3 `errorProps` pode entrar isolada; Fase 1 access dedup é a mais valiosa se coordenadores usarem `/apoiadores` com frequência), **O0+ Fase 4–5** (Lexical/layout shell e Footer condicional — Fases 1–3 preferíveis antes de tráfego real em `/privacidade`), fill-ins (D1 PWA já entregue, saiu da lista de cortes). Preferir manter C6 fases 1–2, C7 fases 1–2, C8 fases 1–2 (perf import/lista), C9 Fase 1 (filtro unificado — evita drift KPI/lista), C10 Fase 1 (access dedup na lista), **O0+ Fase 1** (revalidate globals pós-migration) e **A7 F1** (agregar federal no detalhe) se A5/B3/E2 forem entrar — o custo de I/O do loader do A4 é herdado por eles. **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap, **E1+E3 metas/prioridade/estratégia manual** — são respectivamente o risco legal, a base de dados, a operação da propaganda, o instrumento de alocação de esforço e a paridade mínima com a planilha que a campanha já usa.
+**Cortes seguros se o prazo apertar** (nesta ordem): E5 Salvador por bairro (já registrado como futuro), B4 camada de zonas, B5 F2 (cache CLI — duplicação pequena), E4 import da planilha (os campos E1/E3 podem ser preenchidos à mão pela coordenação; o import só acelera), A7 F2–F3 (`cityCode` / Alert DRY — só se a geografia real dos núcleos permanecer 1–2 municípios), E2 série 2014/2018 (reduzível a só 2018 se o formato TSE de 2014 atrasar; a tendência vira 2 pontos), B3 mapa Leaflet (preferir levar B5 F1 lazy load junto se B3 entrar), C4 demandas, D2 push (mantendo o sino), C6 fases 3–5 (import bulk / preview token / KPI — só se a base nominal permanecer pequena), C7 fases 4–5 restantes (leituras por aba / short-circuit de hooks / feed O(n) — só se a agenda permanecer pequena; contadores + locks já entregues), C8 (escala/DRY pós-C6 — só se a base nominal permanecer pequena; Fases 3–4 DRY podem entrar isoladas), C9 (escala/DRY pós-C8 — só se a base nominal permanecer pequena; Fases 3–4 DRY de forms podem entrar isoladas), C10 (escala/DRY pós-C9 — só se a base nominal permanecer pequena; Fase 3 `errorProps` pode entrar isolada; Fase 1 access dedup é a mais valiosa se coordenadores usarem `/apoiadores` com frequência), **O0+ Fase 4–5** (Lexical/layout shell e Footer condicional — Fases 1–3 preferíveis antes de tráfego real em `/privacidade`), **VR+ Fases 3–5** (row shell, layout wrapper, micro-perf — Fase 1 refresh bfcache é a mais valiosa se usuários reclamarem de painel stale), fill-ins (D1 PWA já entregue, saiu da lista de cortes). Preferir manter C6 fases 1–2, C7 fases 1–2, C8 fases 1–2 (perf import/lista), C9 Fase 1 (filtro unificado — evita drift KPI/lista), C10 Fase 1 (access dedup na lista), **O0+ Fase 1** (revalidate globals pós-migration) e **A7 F1** (agregar federal no detalhe) se A5/B3/E2 forem entrar — o custo de I/O do loader do A4 é herdado por eles. **Não cortáveis:** Onda 0 (jurídico/Consent), C2 cadastro de apoiadores, C3 eventos/agenda, A4 baseline + gap, **E1+E3 metas/prioridade/estratégia manual** — são respectivamente o risco legal, a base de dados, a operação da propaganda, o instrumento de alocação de esforço e a paridade mínima com a planilha que a campanha já usa.
### Itens consolidados/removidos nesta revisão (2026-07-17)
diff --git a/src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx
index ffd69cdf..e4c37ce6 100644
--- a/src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx
@@ -17,6 +17,7 @@ import { CoordinatorAssignmentCard } from '@/components/campaign/CoordinatorAssi
import { CoordinatorAssignmentDialog } from '@/components/campaign/CoordinatorAssignmentDialog'
import { NucleusActiveTab, NucleusActiveTabLoading } from '@/components/campaign/NucleusActiveTab'
import { NucleusTabNav } from '@/components/campaign/NucleusTabNav'
+import { RecentVisitTracker } from '@/components/campaign/RecentVisitTracker'
import { ShareNucleusDialogShell } from '@/components/campaign/ShareNucleusDialogShell'
import { TseZoneBadge } from '@/components/campaign/TseZoneBadge'
import { VoteEstimateCard } from '@/components/campaign/VoteEstimateCard'
@@ -213,6 +214,13 @@ export default async function NucleusDetailPage({ params, searchParams }: Nucleu
view={view}
/>
+
)
}
diff --git a/src/app/(campaign)/campanha/(app)/nucleos/page.tsx b/src/app/(campaign)/campanha/(app)/nucleos/page.tsx
index c0242f4c..77d5d877 100644
--- a/src/app/(campaign)/campanha/(app)/nucleos/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/nucleos/page.tsx
@@ -10,6 +10,7 @@ import { CampaignListPagination } from '@/components/campaign/CampaignListPagina
import { NucleusFilters } from '@/components/campaign/NucleusFilters'
import { NucleusList } from '@/components/campaign/NucleusList'
import { NucleusListOverview } from '@/components/campaign/NucleusListOverview'
+import { RecentVisitTracker } from '@/components/campaign/RecentVisitTracker'
import { Button } from '@/components/ui/button'
import {
Empty,
@@ -25,6 +26,8 @@ import { loadNucleusListPageData } from '@/utilities/nucleusPageData'
import {
buildNucleusFiltersKey,
buildNucleusListHref,
+ buildNucleusListVisitHref,
+ buildNucleusListVisitLabel,
getCampaignScopeLabel,
resolveNucleusListUrl,
} from '@/utilities/nucleusUi'
@@ -52,6 +55,7 @@ export default async function NucleiPage({ searchParams }: NucleiPageProps) {
if (resolvedUrl.redirectHref) redirect(resolvedUrl.redirectHref)
const { state } = resolvedUrl
+ const listVisitLabel = buildNucleusListVisitLabel(state)
return (
@@ -115,6 +119,15 @@ export default async function NucleiPage({ searchParams }: NucleiPageProps) {
)}
+ {listVisitLabel ? (
+
+ ) : null}
)
}
diff --git a/src/components/campaign/CampaignDashboard.tsx b/src/components/campaign/CampaignDashboard.tsx
index f178796a..4a36b3f4 100644
--- a/src/components/campaign/CampaignDashboard.tsx
+++ b/src/components/campaign/CampaignDashboard.tsx
@@ -10,6 +10,7 @@ import Link from 'next/link'
import { CampaignScopeBadge } from '@/components/campaign/CampaignScopeBadge'
import { NucleusCard } from '@/components/campaign/NucleusCard'
+import { RecentlyVisited } from '@/components/campaign/RecentlyVisited'
import { SupportStatusBadge } from '@/components/campaign/SupportStatusBadge'
import { TseZoneBadge } from '@/components/campaign/TseZoneBadge'
import { Badge } from '@/components/ui/Badge'
@@ -160,7 +161,7 @@ const UpcomingActionPlansCard = ({ plans }: { plans: ActionPlanUpcomingPreviewRe
)
-const GeneralDashboard = ({ view }: { view: GeneralDashboardViewModel }) => (
+const GeneralDashboard = ({ view, now }: { view: GeneralDashboardViewModel; now: Date }) => (