diff --git a/README.md b/README.md
index 73657f4..eccea02 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Sistema web para controle de custos de entregas hospitalares da **Gade Hospitalar**.
**Stack:** Ruby on Rails 7+ · PostgreSQL 15 · Docker · Tailwind CSS · Hotwire (Turbo + Stimulus)
-**Repositório:** https://git.xenserver.com.br/Cludio-code/logistica-controle-custos
+**Repositório:** https://git.xenserver.com.br/victor/Reem-Notas
---
@@ -2928,7 +2928,7 @@ todos **só texto**, com cores fixas no renderizador.
| # | Ideia | Esforço | Observação |
|---|---|---|---|
-| B1 | **Seletor de emoji** nos campos de texto — hoje só colando do sistema. Emoji é o que dá cara de WhatsApp à mensagem. | baixo | Um picker pequeno, sem dependência externa (a CSP é restritiva). Inserir no cursor: a mecânica já existe, é a mesma dos chips de variável. |
+| B1 | ✅ **FEITO (26/08/2026)** — Seletor de emoji nos campos de texto. | baixo | Um picker pequeno, sem dependência externa (a CSP é restritiva). Inserir no cursor: a mecânica já existe, é a mesma dos chips de variável. |
| B2 | **Bloco de imagem no e-mail** — `
` com URL ou upload. | médio | Cliente de e-mail bloqueia imagem remota por padrão → precisa de URL absoluta pública ou base64. |
| B3 | **Bloco de imagem no WhatsApp** | **alto** | A ponte hoje só faz `sendMessage` de **texto**. Exige `sendMessage(jid, { image })` no `server.js`, endpoint novo, envio multipart no `ClienteWhatsapp`, storage do arquivo e uma coluna no log de envios. **É o item mais caro da lista** — vale só se a demanda for real. |
| B4 | **Formatação do WhatsApp** (`*negrito*`, `_itálico_`, `~riscado~`, ``` `mono` ```) com botões — hoje o ADM precisa saber a sintaxe de cor. | baixo | No e-mail cada marca vira a tag equivalente. |
@@ -2945,3 +2945,126 @@ no dia a dia. **B3 (imagem no WhatsApp) fica por último** — é o único que m
Ruby e no schema ao mesmo tempo.
+
+---
+
+
+🔐 Atualização 26/08/2026 — Perfis de acesso, variáveis de mensagem e correção de vazamento nos erros
+
+Três frentes numa rodada só: **quem vê o quê** (perfis), **o que dá para escrever numa mensagem**
+(variáveis + emoji) e **o que o sistema mostrava a mais quando dava erro** (segurança).
+
+---
+
+## 1. Perfis de acesso — o ADM monta o acesso sem deploy
+
+**Antes:** o acesso saía de `users.role` (admin/gerente/operador/motorista/externo) e de
+`if admin? || gerente?` espalhado pelas policies. Tirar as Configurações de **um** gerente exigia
+editar código e subir versão.
+
+**Agora:** `Administração → Perfis de acesso`. O ADM cria perfis marcando caixas agrupadas por área
+e atribui o perfil à pessoa em `Usuários`.
+
+| Peça | Onde | O que faz |
+|---|---|---|
+| Catálogo de permissões | `app/models/permissao.rb` | **33 chaves** em 4 grupos (`dashboard`, `consolidacao`, `notificacao`, `admin`), cada uma com o **efeito real** escrito ("Baixar a planilha da operação — é dado saindo do sistema"). É código, não tabela: cada chave corresponde a um `authorize` de verdade. |
+| Perfil | `app/models/perfil_acesso.rb` + `perfis_acesso` | Nome, descrição, lista de permissões (jsonb), `sistema` (não apagável) e `ativo`. |
+| Ligação | `users.perfil_acesso_id` | **Nulo é válido**: sem perfil, vale o padrão do papel antigo (`Permissao::PADRAO_POR_ROLE`) — é o que faz ninguém perder acesso no dia do deploy. |
+| Régua | `User#pode?('chave')` | Todas as 13 policies e a navbar passaram a perguntar por **permissão**, não por papel. |
+
+**Tipo de conta ≠ perfil de acesso.** O select antigo se chamava "Perfil de acesso" mas editava
+`role`. Agora são dois campos: **Tipo de conta** decide *como a pessoa entra* (motorista entra por
+PIN e tem painel próprio); **Perfil de acesso** decide *o que ela enxerga*.
+
+**Migração automática** (`20260826000001..3`): cria os perfis `Administrador`, `Gerente`,
+`Operador` e `Externo` a partir do comportamento atual e atribui a cada usuário o perfil do papel
+que ele já tinha.
+
+### Três falhas de acesso corrigidas junto
+
+| Falha | O que acontecia | Correção |
+|---|---|---|
+| **Auto-promoção a admin** | `UserPolicy#update?` libera editar a própria ficha e o form aceitava `role` → qualquer usuário logado virava admin editando o próprio cadastro. | `role` e `perfil_acesso_id` só entram nos strong params de quem tem `admin.usuarios_gerenciar`, e **nunca sobre si mesmo** (`UserPolicy#alterar_acesso?`). |
+| **`externo` via o financeiro** | `DashboardController` usava `skip_authorization`: o papel "Externo (só dashboard)" enxergava custo, pagamento e o PDF financeiro. | Dashboard passou pelo Pundit; sem `dashboard.financeiro` os blocos de custo **nem são carregados**. |
+| **Conta inativa logava** | Só o login por PIN checava `ativo?`; conta de e-mail/senha desativada entrava normalmente. | `User#active_for_authentication?` (Devise) + mensagem em pt-BR. |
+
+### Duas travas que evitam tiro no pé
+
+- **Último administrador** (`User.administradores_de_acesso`): não dá para salvar um perfil, trocar
+ alguém de perfil, desativar ou excluir usuário se isso deixar o sistema **sem ninguém ativo**
+ capaz de mexer em perfis e usuários. Sem isso, a volta seria só por console.
+- **Tela `/sem-acesso`**: a raiz do app é o dashboard e o "acesso negado" mandava para a raiz — um
+ perfil sem `dashboard.ver` entraria em **loop** (nega → raiz → nega). Agora o destino é a primeira
+ tela que a pessoa pode abrir (`User#home_rota`), e quem não pode abrir nenhuma cai numa página que
+ explica a quem pedir.
+
+---
+
+## 2. Variáveis de mensagem — de 6 fixas para 31 + as suas
+
+**Antes:** `Notificacao::Variaveis::POR_GATILHO` amarrava a lista ao gatilho. O ADM via 6 variáveis
+e qualquer texto com número ("entregas do mês") exigia deploy.
+
+**Agora:** `Notificacao::CatalogoVariaveis`, com quatro origens — e o editor mostra **todas**,
+agrupadas:
+
+| Origem | Exemplos | Observação |
+|---|---|---|
+| **Do que aconteceu** | `{{motorista}}`, `{{valor}}`, `{{consolidacao}}`, `{{nf}}` | Só têm valor no gatilho que as produz. As de outro gatilho aparecem **apagadas com "·"**, avisando que sairiam em branco — liberdade com aviso, em vez de lista curta. |
+| **Empresa e data** | `{{empresa}}`, `{{data}}`, `{{hora}}`, `{{dia_semana}}`, `{{mes}}`, `{{mes_ano}}`, `{{link_sistema}}` | Sempre disponíveis. |
+| **Números do sistema** | `{{entregas_mes}}`, `{{entregas_hoje}}`, `{{motoristas_ativos_mes}}`, `{{consolidacoes_abertas}}`, `{{total_a_pagar_mes}}`, `{{total_pago_mes}}`, `{{total_consolidado_mes}}` | **Consultam o banco na hora do envio.** É o que permite mandar relatório por WhatsApp/e-mail sem programação. |
+| **Suas variáveis** | `{{telefone_suporte}}`, `{{horario_atendimento}}`… | Tela nova: `Notificações → Variáveis`. Valor fixo, mudou ali mudou em todas as mensagens. |
+
+**Duas decisões que importam:**
+
+1. **Resolução sob demanda** (`Notificacao::ResolvedorVariaveis`): só a variável realmente escrita no
+ template vira consulta. Resolver o catálogo inteiro em todo disparo faria dezenas de queries para
+ preencher variável que ninguém usou.
+2. **O contexto do gatilho sempre vence** o catálogo — o `{{valor}}` daquele pagamento nunca é
+ trocado por um número genérico. E nenhuma variável derruba um disparo: falha vira texto vazio.
+
+**Emoji** (item B1 do backlog abaixo — ✅ feito): paleta de 27 emojis no editor, insere no cursor
+reusando a mecânica dos chips de variável. Sem dependência externa, por causa da CSP.
+
+---
+
+## 3. Segurança — o que a página de erro mostrava
+
+O servidor sobe com `RAILS_ENV=development` (`docker-compose.yml`), e development tinha
+`consider_all_requests_local = true`. A página **"Action Controller: Exception caught"** mostra
+parâmetros, **sessão** (`session_id`, `_csrf_token`, id do usuário logado), cookies, IP do cliente,
+caminho do servidor e o trace inteiro — para **qualquer pessoa** que provocasse um erro.
+
+| Problema | Correção |
+|---|---|
+| Painel de debug na tela do usuário | Detalhe só com `ERROS_DETALHADOS=true` no `.env` (máquina de desenvolvimento). No servidor, sai a página estática. |
+| **Senha e PIN em texto puro no log** — não existia `filter_parameter_logging.rb` neste projeto | Criado, com nomes em português e inglês (`senha`, `pin_code`, `password`, `token`, `whatsapp_token`…). **Era o pior dos três**: log é permanente e vai junto em backup e em suporte. |
+| Tela em branco ao desligar o painel | `public/500.html`, `404.html` e `422.html` em português, sem CSS externo (precisam funcionar com a aplicação fora do ar). |
+| Páginas de debug salvas versionadas | `Erros/` saiu do git e entrou no `.gitignore`. Nos arquivos há `session_id`, token CSRF, id de usuário e IPs — **não há cookie de sessão assinado**, então ninguém entra no sistema com aquilo, mas não é conteúdo de repositório. |
+
+> ⚠️ **Correção estrutural ainda pendente:** subir o servidor com `RAILS_ENV=production`
+> (`production.rb` já tem `consider_all_requests_local = false` e `force_ssl = true`). Enquanto isso
+> não acontece, o `ERROS_DETALHADOS` cobre o buraco.
+
+---
+
+## 4. Telas que mudaram nesta rodada
+
+- **Dashboard de Operações**: no modo Operação o período **não recorta mais** os KPIs — eles voltam a
+ bater 1-para-1 com a planilha entregue ao cliente. Uma operação mensal executa entregas fora do mês
+ do nome (a `gade_entregas_emad_ago_2026` rodou de 30/07 a 12/08): filtrar "01/08 → hoje" mostrava
+ **1146 das 2021 NFs**. O período escolhido virou referência no cabeçalho.
+- **Consolidações**: aba **"Por motorista"** na própria tela (não é tela nova), com o total de cada
+ um, drill-down inline mostrando as consolidações que compõem o valor, PDF completo/resumo/por
+ motorista, e o filtro **Consolidado (fechadas) × Geral (com rascunhos)**. O recorte de datas foi
+ unificado com o do dashboard financeiro (consolidações que **cruzam** o período + as pagas nele),
+ que era a origem de dois valores diferentes para o mesmo motorista.
+- **Painel do motorista**: refeito. Período em chips (Este mês / Mês passado / 3 meses / Tudo), card
+ do valor **fechado** com "já recebido" e "a receber", PDF do período, e a composição do valor.
+ O card de estimativa **nunca aparece junto do fechado** (o estimado costuma ser maior e virava
+ cobrança); com o período já fechado, ele dá lugar à contagem de entregas, sem R$.
+- **Listas longas** (`carrossel_controller.js`): 5 itens por página no celular, 10 até 1366px, lista
+ inteira acima disso. As setas flutuam nas laterais em tela com espaço e viram barra no rodapé no
+ celular, onde não há faixa lateral livre.
+
+
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 57f3ef6..d629526 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -9,4 +9,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
# Sem isto, "perfis".singularize vira "perfi" e as rotas de perfil de acesso
# gerariam helpers como `new_admin_perfi_path`.
inflect.irregular 'perfil', 'perfis'
+ # Idem para variável: sem esta linha "variaveis".singularize vira "variavei" e
+ # `resources :variaveis` gera `new_admin_variavei_path` — a tela quebra com
+ # NameError na primeira chamada de rota.
+ inflect.irregular 'variavel', 'variaveis'
end
diff --git a/db/migrate/20260826000005_sincronizar_permissoes_do_perfil_administrador.rb b/db/migrate/20260826000005_sincronizar_permissoes_do_perfil_administrador.rb
new file mode 100644
index 0000000..6d7b62b
--- /dev/null
+++ b/db/migrate/20260826000005_sincronizar_permissoes_do_perfil_administrador.rb
@@ -0,0 +1,40 @@
+# Garante que o perfil "Administrador" tenha TODAS as permissões do catálogo.
+#
+# Por que existe: 20260826000003 é idempotente — se o perfil já foi criado, ela
+# não volta a mexer nele. Isso é o certo para não desfazer ajuste do ADM, mas
+# significa que uma permissão acrescentada ao catálogo DEPOIS daquele deploy
+# (foi o caso de `notificacao.variaveis`) nunca chega ao perfil: o item some do
+# menu e a URL responde "sem permissão", sem nada indicando o porquê.
+#
+# Mexe SÓ no perfil Administrador, e SÓ acrescentando: ele é, por definição,
+# "acesso total ao sistema". Perfis criados pelo ADM e os demais de sistema
+# ficam como estão.
+class SincronizarPermissoesDoPerfilAdministrador < ActiveRecord::Migration[7.1]
+ TODAS = %w[
+ dashboard.ver dashboard.financeiro dashboard.operacoes dashboard.exportar dashboard.metricas
+ consolidacao.ver consolidacao.criar consolidacao.editar consolidacao.editar_finalizada
+ consolidacao.finalizar consolidacao.arquivar consolidacao.excluir
+ consolidacao.registrar_pagamento consolidacao.cancelar_pagamento
+ consolidacao.gerir_motoristas consolidacao.exportar_pdf
+ notificacao.contatos notificacao.contatos_gerenciar notificacao.contatos_excluir
+ notificacao.eventos notificacao.eventos_gerenciar notificacao.disparar_manual
+ notificacao.variaveis notificacao.envios notificacao.credenciais notificacao.whatsapp_sessao
+ admin.usuarios admin.usuarios_gerenciar admin.perfis
+ admin.configuracoes admin.configuracoes_editar admin.auditoria
+ admin.edicao_lancamento admin.planilha_simpli_route
+ ].freeze
+
+ def up
+ return unless table_exists?(:perfis_acesso)
+
+ execute(<<~SQL.squish)
+ UPDATE perfis_acesso
+ SET permissoes = #{quote(TODAS.to_json)}::jsonb, updated_at = NOW()
+ WHERE sistema = true AND LOWER(nome) = 'administrador'
+ SQL
+ end
+
+ # Sem volta: reduzir as permissões do administrador só criaria o problema que
+ # esta migration conserta.
+ def down; end
+end