diff --git a/README.md b/README.md
index 76dc5ad..45b2dbc 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ docs/ decisões de modelagem (DECISOES.md), glossário, BPMN, resultados
genai/ formato JSON, regras, normas, prompts e código do parecer
notebooks/ EDA e etapas do modelo (coortes, variáveis, XGBoost, LGD, margem)
src/api/ API FastAPI
-src/app/ painel Streamlit
+src/app/ painel Streamlit (main.py = telas, estilo.css = visual, componentes.py = blocos HTML)
src/dados/ cargas do Bondora e das séries de juros
src/modelos/ modelo de risco e juro justo (prever())
modelos_salvos/ pacote do modelo (.joblib, fora do Git)
@@ -62,11 +62,11 @@ tests/ testes
Pré-requisitos: Git, Python 3.14 e Docker Desktop.
-**Atalho (Windows):** com o banco já carregado (passos 2 a 5 abaixo, uma vez), use o `iniciar.ps1`:
+**Atalho (Windows):** clique duplo no `iniciar.bat` (ou `.\iniciar.ps1`). Num PC novo basta Docker Desktop, `.env`, o `.joblib` do modelo e os 4 CSVs de custo do dinheiro em `data/raw/` (para a referência do BC): veja [`docs/RODAR_EM_QUALQUER_PC.md`](docs/RODAR_EM_QUALQUER_PC.md). A base do Bondora só é necessária para refazer os dados e o modelo (passos 2 a 5 abaixo).
| Comando | O que faz |
| --- | --- |
-| `.\iniciar.ps1` | confere `.env` e modelo, sobe o banco, aplica a tabela de decisões, indexa as normas (se faltar), sobe API e painel e abre o navegador. Constrói a imagem só se ela não existir. |
+| `.\iniciar.ps1` | confere portas, `.env` e modelo, sobe o banco, aplica a tabela de decisões, indexa as normas e carrega a referência do BC (se faltarem), sobe API e painel e abre o navegador. Constrói a imagem só se ela não existir. |
| `.\iniciar.ps1 -Build` | o mesmo, reconstruindo a imagem (mudou código ou requirements) |
| `.\iniciar.ps1 -Parar` | para tudo; os dados do banco ficam guardados |
@@ -84,7 +84,7 @@ O primeiro build leva alguns minutos (PyTorch); os seguintes reaproveitam o cach
2. **Dados.** Todos os arquivos ficam numa pasta compartilhada do Drive, fora do Git (o repositório é público). Peça o link ao grupo e coloque os arquivos em `data/raw/`, com estes nomes:
- `LoanData_Bondora.csv` (confira o SHA-256 em `DATA.md`)
- - `ECB_Euribor_Serie-Mensal-12-Meses_csv.csv` e `ECB_Euribor_Serie-Mensal-3-Meses.csv`
+ - `ECB_Euribor_Serie-Mensal-12-Meses.csv` (o download do BCE às vezes vem como `..._csv.csv`: os dois nomes funcionam) e `ECB_Euribor_Serie-Mensal-3-Meses.csv`
- `BC-Selic_Acumulada_Mes_Anuzalizada_base252.csv`
- `BC-Meta_Selic_Mensal.csv` (opcional, só conferência)
- `bcdata.sgs.20742.csv` (opcional, referência de mercado do BC)
@@ -143,7 +143,8 @@ O primeiro build leva alguns minutos (PyTorch); os seguintes reaproveitam o cach
- [`genai/FORMATO_JSON.md`](genai/FORMATO_JSON.md) e [`genai/REGRAS_PARECER.md`](genai/REGRAS_PARECER.md): contrato e regras do parecer.
- [`genai/avaliacao/`](genai/avaliacao/CRITERIOS.md): critérios, os 20 casos de avaliação e os resultados do parecer (prompts v2 a v4), com a revisão humana da v4.
- [`docs/ROTEIRO_DEMO.md`](docs/ROTEIRO_DEMO.md): roteiro da demo, plano B e perguntas prováveis.
-- [`infra/`](infra/README.md): o mesmo ambiente em Terraform (provider Docker, local, sem custo).
+- [`infra/`](infra/README.md): Terraform que cria na Google Cloud uma VM rodando o mesmo Docker Compose (para a demo na nuvem).
+- [`docs/RODAR_EM_QUALQUER_PC.md`](docs/RODAR_EM_QUALQUER_PC.md): como rodar em qualquer PC da equipe e a diferença entre Docker e Terraform.
- [`skills/`](skills/README.md): guia do projeto por setor (estrutura, ambiente, dados, modelo, juro justo, parecer, API, painel, governança).
## Governança e ética
@@ -168,6 +169,6 @@ O primeiro build leva alguns minutos (PyTorch); os seguintes reaproveitam o cach
Cada frente registra aqui como usou assistentes de IA (agentic coding) e como revisou o resultado.
- **Dados:** os scripts de carga, os esquemas SQL e a documentação foram escritos com apoio do Claude e revisados e testados pelo responsável da frente.
-- **App e infraestrutura:** a integração da API com o `prever()`, as telas do painel e os testes foram feitos com apoio do Claude, a partir dos protótipos do grupo e dos contratos (`FORMATO_JSON.md`, `REGRAS_PARECER.md`). A responsável revisou o código e conferiu o resultado com testes automáticos e rodando as telas.
+- **App e infraestrutura:** a integração da API com o `prever()`, as telas do painel e os testes foram feitos com apoio do Claude, a partir dos protótipos do grupo e dos contratos (`FORMATO_JSON.md`, `REGRAS_PARECER.md`). A responsável revisou o código e conferiu o resultado com testes automáticos e rodando as telas. O formulário com etapas travadas, a separação do visual em `estilo.css` e os scripts `iniciar` seguiram o mesmo processo.
- **Modelo:** a completar pela frente.
- **GenAI:** o código do parecer, os prompts e os documentos da frente foram escritos com apoio do Claude, guiado pelos guias da frente em `skills/regras_negocio/` ([`normas_rag`](skills/regras_negocio/normas_rag/SKILL.md), [`parecer_genai`](skills/regras_negocio/parecer_genai/SKILL.md) e [`avaliacao_parecer`](skills/regras_negocio/avaliacao_parecer/SKILL.md)) e pelos contratos (`FORMATO_JSON.md`, `REGRAS_PARECER.md`). As propostas maiores (template, cadeia em LangChain, prompts v3 e v4) também passaram pela revisão de uma segunda IA. A responsável leu cada mudança antes de aplicá-la e conferiu o resultado com os testes automáticos (`tests/genai/`), a avaliação dos 20 casos e a revisão humana dos pareceres (`genai/avaliacao/`).
diff --git a/docs/RODAR_EM_QUALQUER_PC.md b/docs/RODAR_EM_QUALQUER_PC.md
new file mode 100644
index 0000000..0210ee5
--- /dev/null
+++ b/docs/RODAR_EM_QUALQUER_PC.md
@@ -0,0 +1,69 @@
+# Rodar o Auditor em qualquer PC da equipe
+
+## Docker e Terraform: para que serve cada um
+
+| | Docker Compose (`iniciar.bat`) | Terraform (`infra/`) |
+| --- | --- | --- |
+| O que faz | sobe banco, API e painel **no seu PC** | cria **uma máquina na Google Cloud** que roda o mesmo Docker Compose |
+| Quando usar | no dia a dia, nos testes e como plano B da demo | para mostrar o sistema na nuvem e a "infraestrutura como código" |
+| Custo | nenhum | gasta o crédito grátis da Google enquanto a máquina existir |
+| Guia | este arquivo | [`infra/README.md`](../infra/README.md) |
+
+**Quem roda a aplicação é sempre o Docker.** O `docker-compose.yml` descreve os 3 containers.
+O Terraform não substitui o Docker: ele cria o *computador* (rede, IP, firewall e VM) onde o Docker vai rodar.
+Em uma frase: **"Docker empacota a aplicação; Terraform cria a infraestrutura onde ela roda."**
+
+## 1. Uma vez por PC
+
+| O quê | De onde |
+| --- | --- |
+| Docker Desktop | docker.com → *Download Docker Desktop* (Windows: aceite o WSL 2). Abra e espere ficar verde. |
+| Git | git-scm.com |
+| Projeto | `git clone https://github.com/yaMimis/Auditor-Project.git` |
+| `.env` | peça à Yasmin (tem a `GOOGLE_API_KEY` e a `CLIENTE_ID_SAL`). Nunca vai para o GitHub. |
+| `modelos_salvos\pacote_prever_v1.joblib` | peça ao Samuel (Drive) |
+| 4 CSVs de custo do dinheiro em `data\raw\` | peça ao Marcos (Drive), nomes abaixo |
+
+Os 4 CSVs (são pequenos, séries mensais do Banco Central e do Banco Central Europeu):
+
+- `ECB_Euribor_Serie-Mensal-12-Meses_csv.csv` (ou sem o `_csv`)
+- `ECB_Euribor_Serie-Mensal-3-Meses.csv`
+- `BC-Selic_Acumulada_Mes_Anuzalizada_base252.csv`
+- `bcdata_sgs_20742.csv` (ou `bcdata.sgs.20742.csv`): a taxa média do crédito pessoal, que vira a "referência de mercado" no parecer
+
+Na primeira vez, o `iniciar` carrega esses arquivos no banco sozinho. Os mesmos 4 arquivos vão para a nuvem (passo 14 do `infra/README.md`).
+
+Os arquivos da aba "Auditoria da carteira" (`docs\resultados\`, gerados pelo Samuel) já vêm com o `git clone`.
+
+A base grande do Bondora (`LoanData`) **não** é necessária para rodar o sistema. Ela só serve para refazer
+os dados e o modelo (passos 2 a 5 do README).
+
+## 2. Todo dia
+
+**Clique duplo no `iniciar.bat`** (ou `.\iniciar.ps1` no PowerShell). Ele:
+
+1. confere se o Docker está ligado e se as portas 5432, 8000 e 8501 estão livres;
+2. confere o `.env` e o modelo;
+3. sobe o banco e aplica a tabela de decisões;
+4. constrói a imagem só se ela não existir;
+5. indexa as normas e carrega a referência do BC, se faltarem;
+6. sobe API e painel e abre http://localhost:8501.
+
+| Situação | Comando |
+| --- | --- |
+| Subir | `iniciar.bat` ou `.\iniciar.ps1` |
+| Veio código novo no `git pull` | `.\iniciar.ps1 -Build` |
+| Desligar (os dados ficam) | `.\iniciar.ps1 -Parar` |
+| Ver o que deu errado | `docker compose logs api --tail 40` |
+
+## 3. Problemas comuns
+
+| Mensagem | O que fazer |
+| --- | --- |
+| "O Docker não está rodando" | Abra o Docker Desktop e espere ficar verde |
+| "A porta X está com o container Y" | `docker stop Y` (o script mostra o comando) e rode de novo |
+| "A porta X está ocupada pelo programa Y" | Feche o programa (o script mostra o comando) |
+| "Sem a referência de mercado do BC" | Coloque os 4 CSVs em `data\raw\` e rode de novo |
+| "não está assinado" / ExecutionPolicy | Use o `iniciar.bat`, ou antes: `Set-ExecutionPolicy Bypass -Scope Process` |
+| "Falta modelos_salvos\...joblib" | Peça o arquivo ao Samuel; até lá, use "Caso de exemplo" |
+| API demora na 1ª vez | Normal: ela baixa o modelo de busca nas normas (~0,5 GB) uma única vez |
diff --git a/infra/outputs.tf b/infra/outputs.tf
index 5735e1e..9e8c78d 100644
--- a/infra/outputs.tf
+++ b/infra/outputs.tf
@@ -34,8 +34,8 @@ output "comando_esperar_vm" {
}
output "comando_enviar_arquivos" {
- description = "Envia o modelo e os CSVs de custo do dinheiro, que não estão no Git"
- value = "scp -i ${local.chave} modelos_salvos/pacote_prever_v1.joblib data/raw/ECB_Euribor_Serie-Mensal-12-Meses_csv.csv data/raw/ECB_Euribor_Serie-Mensal-3-Meses.csv data/raw/BC-Selic_Acumulada_Mes_Anuzalizada_base252.csv data/raw/bcdata_sgs_20742.csv ${var.usuario}@${local.ip}:~/envio/"
+ description = "Envia o modelo e os CSVs de custo do dinheiro, que não estão no Git (nomes como estão no Drive do grupo)"
+ value = "scp -i ${local.chave} modelos_salvos/pacote_prever_v1.joblib data/raw/ECB_Euribor_Serie-Mensal-12-Meses.csv data/raw/ECB_Euribor_Serie-Mensal-3-Meses.csv data/raw/BC-Selic_Acumulada_Mes_Anuzalizada_base252.csv data/raw/bcdata.sgs.20742.csv ${var.usuario}@${local.ip}:~/envio/"
}
output "comando_completar" {
diff --git a/iniciar.bat b/iniciar.bat
new file mode 100644
index 0000000..200100e
--- /dev/null
+++ b/iniciar.bat
@@ -0,0 +1,6 @@
+@echo off
+rem Clique duplo para subir o Auditor de Preco Justo (banco, API e painel).
+rem Mesmo efeito de .\iniciar.ps1; repassa opcoes, ex.: iniciar.bat -Build
+cd /d "%~dp0"
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0iniciar.ps1" %*
+pause
diff --git a/iniciar.ps1 b/iniciar.ps1
index a594366..c40862c 100644
--- a/iniciar.ps1
+++ b/iniciar.ps1
@@ -5,6 +5,7 @@
# .\iniciar.ps1 -Build reconstroi a imagem antes (mudou codigo ou requirements)
# .\iniciar.ps1 -Parar para tudo (os dados do banco continuam no volume)
# .\iniciar.ps1 -SemNavegador nao abre o navegador no final
+# Ou de clique duplo no iniciar.bat (mesmo efeito, sem mexer na ExecutionPolicy).
# Se o Windows bloquear o script, rode antes, na mesma janela:
# Set-ExecutionPolicy Bypass -Scope Process
#
@@ -52,6 +53,21 @@ if ($Parar) {
exit 0
}
+# Portas livres? (5432 banco, 8000 API, 8501 painel). Outro container ou programa nelas impede a subida.
+$nossos = @("auditor_postgres", "auditor_api", "auditor_streamlit")
+$programas_docker = @("com.docker.backend", "wslrelay", "vpnkit", "com.docker.proxy", "dockerd", "docker")
+foreach ($porta in 5432, 8000, 8501) {
+ $outro = docker ps --filter "publish=$porta" --format "{{.Names}}" | Where-Object { $nossos -notcontains $_ }
+ if ($outro) { Parar "A porta $porta esta com o container '$outro'. Pare com: docker stop $outro e rode de novo." }
+ $con = Get-NetTCPConnection -LocalPort $porta -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
+ if ($con) {
+ $proc = Get-Process -Id $con.OwningProcess -ErrorAction SilentlyContinue
+ if ($proc -and ($programas_docker -notcontains $proc.ProcessName)) {
+ Parar "A porta $porta esta ocupada pelo programa '$($proc.ProcessName)'. Feche-o com: Stop-Process -Id $($proc.Id) e rode de novo."
+ }
+ }
+}
+
# 1. .env (fica fora do Git): cria um modelo se nao existir, sem sobrescrever o seu
Passo "1/6 Conferindo o .env e o modelo"
if (-not (Test-Path ".env")) {
@@ -93,10 +109,6 @@ docker exec auditor_postgres psql -q -U postgres -d auditor_db -v ON_ERROR_STOP=
if ($LASTEXITCODE -ne 0) { Parar "Erro ao aplicar db\05_decisoes_analista.sql. Rode o comando a mao para ver a mensagem." }
Ok "tabela decisao_analista"
-$contratos = docker exec auditor_postgres psql -tA -U postgres -d auditor_db -c "SELECT count(*) FROM emprestimo" 2>$null
-if ("$contratos".Trim() -eq "0") {
- Aviso "O banco esta sem os contratos (cargas do README, passo 5). O painel funciona, mas sem a referencia de mercado do BC."
-}
# 4. Imagem: so constroi se nao existir ou se pedirem -Build
Passo "4/6 Imagem da aplicacao"
@@ -110,8 +122,8 @@ if ($Build -or $LASTEXITCODE -ne 0) {
Ok "imagem ja existe (use -Build se mudou codigo ou requirements)"
}
-# 5. Normas do parecer no pgvector (dentro do container: nao precisa de Python local)
-Passo "5/6 Normas do parecer"
+# 5. Normas do parecer no pgvector e referencia de mercado do BC (dentro do container: sem Python local)
+Passo "5/6 Normas do parecer e referencia do BC"
$trechos = docker exec auditor_postgres psql -tA -U postgres -d auditor_db -c "SELECT count(*) FROM trechos_normas" 2>$null
if ("$trechos".Trim() -match "^[1-9]") {
Ok "normas ja indexadas ($("$trechos".Trim()) trechos)"
@@ -121,6 +133,23 @@ if ("$trechos".Trim() -match "^[1-9]") {
else { Ok "normas indexadas" }
}
+# Referencia do BC: series mensais de 4 CSVs pequenos em data\raw (a base do Bondora nao e necessaria).
+# Mesma carga que o completar.sh faz na VM da nuvem (infra/cloud-init.yaml).
+$meses_bc = docker exec auditor_postgres psql -tA -U postgres -d auditor_db -c "SELECT count(*) FROM bc_credito_pessoal_mensal" 2>$null
+if ("$meses_bc".Trim() -match "^[1-9]") {
+ Ok "referencia do BC ja carregada ($("$meses_bc".Trim()) meses)"
+} elseif (-not ((Test-Path "data\raw\bcdata.sgs.20742.csv") -or (Test-Path "data\raw\bcdata_sgs_20742.csv"))) {
+ Aviso "Sem a referencia de mercado do BC: falta data\raw\bcdata.sgs.20742.csv (baixe em https://api.bcb.gov.br/dados/serie/bcdata.sgs.20742/dados?formato=csv) e rode de novo."
+} elseif ((Test-Path "data\raw\ECB_Euribor_Serie-Mensal-3-Meses.csv") -and
+ (Test-Path "data\raw\BC-Selic_Acumulada_Mes_Anuzalizada_base252.csv")) {
+ $pasta_raw = (Resolve-Path "data\raw").Path
+ docker compose run --rm --no-deps -v "${pasta_raw}:/app/data/raw:ro" api python src/dados/carga_macro.py --ddl db/02_schema_macro.sql
+ if ($LASTEXITCODE -ne 0) { Aviso "A carga das series do BC falhou (veja a mensagem acima). O painel funciona sem a referencia de mercado." }
+ else { Ok "Euribor, Selic e taxa do BC carregadas" }
+} else {
+ Aviso "Sem a referencia de mercado do BC. Coloque em data\raw os CSVs da Euribor (12 e 3 meses) e da Selic (peca ao Marcos) e rode de novo."
+}
+
# 6. API e painel em segundo plano; espera a API ficar saudavel
Passo "6/6 Subindo a API e o painel"
docker compose up -d api streamlit
diff --git a/requirements.txt b/requirements.txt
index 5b41d0a..2b98d4f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,7 @@ requests>=2.31.0
python-dotenv
# Painel
-streamlit>=1.37.0
+streamlit>=1.44.0 # st.container(key=...) e ícones nos botões (etapas do formulário)
# Banco de Dados
sqlalchemy>=2.0.0
diff --git a/skills/regras_negocio/painel/SKILL.md b/skills/regras_negocio/painel/SKILL.md
index 175c8e8..7d0bc2a 100644
--- a/skills/regras_negocio/painel/SKILL.md
+++ b/skills/regras_negocio/painel/SKILL.md
@@ -5,7 +5,7 @@ description: Use para construir ou alterar o painel Streamlit do Auditor de Pre
# Painel do analista (Streamlit)
-> Leia antes: `regras_negocio/governanca`, `regras_negocio/contratos_dados`, `regras_negocio/api`. Dono: Yasmin. Código: `src/app/main.py` (tela), `src/app/componentes.py` (formatação e HTML, sem Streamlit), `src/app/carteira.py` (leitura e resumo do lote); testes `tests/app/`.
+> Leia antes: `regras_negocio/governanca`, `regras_negocio/contratos_dados`, `regras_negocio/api`. Dono: Yasmin. Código: `src/app/main.py` (telas), `src/app/estilo.css` (todo o CSS; o `main.py` só injeta), `src/app/componentes.py` (formatação e HTML, sem Streamlit), `src/app/carteira.py` (leitura e resumo do lote); testes `tests/app/`.
## Objetivo
@@ -28,11 +28,13 @@ Dar ao analista, em uma tela, **o risco, o preço, o porquê e o parecer** de ca
### Aba 1 — Revisão de contrato
- Origem: **Novo contrato (formulário)** ou **Caso de exemplo**: os 8 casos reais de `docs/casos_de_teste.json` (reserva: `docs/exemplos_prever.json`), abertos com parecer via `/parecer`.
-- **Formulário em 3 etapas**, com "Confirmo que revisei" por etapa, barra de progresso REVISADO/PENDENTE e resumo do pedido:
+- **Formulário em 3 etapas travadas**, com "Confirmo que revisei" por etapa, barra de progresso REVISADO/PENDENTE e resumo do pedido:
+ - uma etapa só abre com todas as anteriores revisadas (🔒 e botão desabilitado); voltar é sempre permitido; "Avançar para …" só com a caixa marcada;
+ - mudar um campo depois de revisado **desmarca** a revisão daquela etapa; desmarcar uma etapa anterior leva de volta à primeira pendente;
- 01 Dados pessoais: idade, gênero (0 Masculino · 1 Feminino · 2 Não informado; só auditoria), país (EE/FI/ES), renda mensal (€)
- 02 Empréstimo: ID, valor (€), juro cobrado (% a.a.), prazo (3–60 meses), data de concessão (01/01/2009 até hoje; padrão hoje)
- 03 Dívidas & histórico: total de dívidas (€); os demais campos seguem "sem informação"
- - Botão "⚖️ EXECUTAR AUDITORIA DE PREÇO".
+ - Botão "⚖️ EXECUTAR AUDITORIA DE PREÇO" **desabilitado até as 3 caixas**, com o aviso "Falta revisar: …"; o código também confere antes de chamar a API.
- **Resultado:**
- cabeçalho: contrato, país, data, valor, prazo, etiqueta da situação, chips ("Custo do dinheiro: Euribor da época", "Parecer: análise hipotética pela lei brasileira", versão do modelo, "Caso real da base", "Exemplo ilustrativo" ou "Aberto da carteira");
- 4 cartões: risco de inadimplência (PD, IC 95% quando houver, faixa) · juro cobrado · juro justo com margem · situação do preço (cor da situação, pp além da margem);
@@ -67,6 +69,9 @@ Cartões (decisões, diferentes da recomendação, encaminhadas ao compliance),
| Acima do justo | `#e8a462` · Abaixo `#c3a6f0` · Dentro `#8cc7a1` · Revisão manual `#9aa5b5` |
| Aumenta / reduz risco | `#d9825b` / `#7fa7d9` |
| Fontes | títulos Playfair Display; texto DM Sans |
+| Abas principais (Revisão · Carteira · Registro) | cartões de 104 px, gradiente escuro; aba ativa com gradiente dourado `#c89a58 → #e4bd7d` e texto `#171315` |
+| Etapas do formulário (Dados pessoais · Empréstimo · Dívidas) | abas de texto pequenas, alinhadas à esquerda, sem caixa: cinza `#9aa5b5`, hover só muda a cor, ativa dourada `#e4bd7d` com traço de 2 px `#c89a58`; travada com opacidade 38% |
+| Ícones | nada de emoji: ícones de linha brancos (`componentes.icone()`, SVG) no HTML e Material (`icon=":material/...:"`) nos botões; etapa revisada `check_circle`, travada `lock` |
Tema do Streamlit (`.streamlit/config.toml`): `base="dark"`, `primaryColor="#D4AD70"`, `backgroundColor="#080D16"`, `secondaryBackgroundColor="#101A2A"`, `textColor="#F1F0EB"`, `headless=true`, `gatherUsageStats=false`.
@@ -81,3 +86,8 @@ Tema do Streamlit (`.streamlit/config.toml`): `base="dark"`, `primaryColor="#D4A
- **[A CONFIRMAR] (Marcos)** rótulos dos códigos de escolaridade, moradia e verificação (`CODIGOS` em `componentes.py`, tirados do dicionário do Bondora).
- **[PENDENTE] (Yasmin)** mostrar "a.a. nominal" ao lado dos juros.
- **[PENDENTE] (depende de outras frentes)** `juro_pares_mediana` como referência ao lado do juro justo ("Mediana dos pares: X%", oculto quando `null`; até 09/11); `contexto_mercado` do BC no parecer; decisões gravadas no banco e marcação "já decidido" na fila; dados reais da carteira (08/11 e 09/11).
+
+## Estado em 10/11/2026
+
+- **[PRONTO]** emojis trocados por ícones brancos; CSS separado em `estilo.css`; abas principais como cartões e etapas como abas de texto pequenas; etapas travadas e execução só com as 3 confirmações.
+- **[DECIDIDO]** sem tela de login: o sistema roda no PC de cada pessoa e o nome do analista é digitado (obrigatório para decidir). Login só faria sentido com o site publicado.
diff --git a/src/app/componentes.py b/src/app/componentes.py
index ed3091f..fe84c17 100644
--- a/src/app/componentes.py
+++ b/src/app/componentes.py
@@ -394,4 +394,26 @@ def html_tabela_equidade(linhas: list[dict]) -> str:
Custo do dinheiro (Euribor da época), perda esperada ao longo do contrato e custos calibrados na carteira, com margem de incerteza.
-
🤖
+
{icone('chip', 26)}
Modelo de risco explicável
XGBoost calibrado estima a probabilidade de default; os principais fatores de cada contrato aparecem com nomes legíveis.
-
📜
+
{icone('documento', 26)}
Parecer com normas e checagem
IA generativa com busca em CDC, LGPD e normas do CMN. Os números do texto são conferidos por código; se algo falhar, entra o template.
@@ -494,8 +295,42 @@ def csv_da_fila(anos, pais, faixa) -> bytes:
# ------------------------------------------
# 6.1 Formulário em etapas
# ------------------------------------------
+ETAPAS = ("personal", "loan", "history")
+CAMPOS_DA_ETAPA = {
+ "personal": ("age", "gender", "country", "income_total"),
+ "loan": ("loan_id", "amount", "interest", "loan_duration", "data_emprestimo"),
+ "history": ("liabilities_total",),
+}
+ETAPA_DO_CAMPO = {campo: etapa for etapa, campos in CAMPOS_DA_ETAPA.items() for campo in campos}
+
+
+def _desmarcar_revisao(step_name: str) -> None:
+ st.session_state[f"audit_reviewed_{step_name}"] = False
+ if f"_audit_review_widget_{step_name}" in st.session_state:
+ st.session_state[f"_audit_review_widget_{step_name}"] = False
+
+
def _persist_audit_input(field_name: str) -> None:
st.session_state[f"audit_{field_name}"] = st.session_state[f"_audit_widget_{field_name}"]
+ # dado alterado depois de revisado → a etapa precisa ser revisada de novo
+ _desmarcar_revisao(ETAPA_DO_CAMPO.get(field_name, "history"))
+
+
+def etapa_revisada(step_name: str) -> bool:
+ return bool(st.session_state.get(f"audit_reviewed_{step_name}"))
+
+
+def etapa_liberada(step_name: str) -> bool:
+ """Uma etapa só abre quando todas as anteriores estão marcadas como revisadas."""
+ return all(etapa_revisada(e) for e in ETAPAS[:ETAPAS.index(step_name)])
+
+
+def todas_revisadas() -> bool:
+ return all(etapa_revisada(e) for e in ETAPAS)
+
+
+def primeira_pendente() -> str:
+ return next((e for e in ETAPAS if not etapa_revisada(e)), ETAPAS[-1])
def _prepare_audit_widget(field_name: str) -> str:
@@ -542,8 +377,18 @@ def audit_review_checkbox(step_name: str) -> None:
on_change=_persist_review, args=(step_name,))
-STEP_CARDS = [("personal", "\n\nDADOS PESSOAIS\n\n"), ("loan", "\n\nEMPRÉSTIMO\n\n"),
- ("history", "\n\nDÍVIDAS & HISTÓRICO\n\n")]
+def botao_proxima(step_name: str) -> None:
+ """Atalho para a etapa seguinte; só funciona com a etapa atual marcada como revisada."""
+ seguinte = ETAPAS[ETAPAS.index(step_name) + 1]
+ rotulo = dict(STEP_CARDS)[seguinte]
+ if st.button(f"Avançar para {rotulo}", key=f"audit_next_{step_name}", icon=":material/arrow_forward:",
+ disabled=not etapa_revisada(step_name)):
+ st.session_state.audit_active_step = seguinte
+ st.rerun()
+
+
+STEP_CARDS = [("personal", "Dados pessoais"), ("loan", "Empréstimo"), ("history", "Dívidas & histórico")]
+ICONES_ETAPA = {"personal": ":material/person:", "loan": ":material/payments:", "history": ":material/history:"}
STEP_DETAILS = {
"personal": {"number": "01", "title": "Dados pessoais",
"copy": "Perfil do tomador no momento do pedido. Gênero e idade ficam registrados só para a auditoria de equidade e nunca entram no modelo."},
@@ -556,13 +401,25 @@ def audit_review_checkbox(step_name: str) -> None:
def formulario() -> bool:
"""Desenha o formulário em 3 etapas. Devolve True quando o botão de auditoria é clicado."""
- _, nav_center, _ = st.columns([0.35, 2.3, 0.35], gap="small")
- with nav_center:
+ # Etapa aberta que deixou de estar liberada (ex.: desmarcou a revisão anterior) → volta
+ # para a primeira etapa pendente. Voltar é sempre permitido; avançar, só revisando.
+ if not etapa_liberada(st.session_state.audit_active_step):
+ st.session_state.audit_active_step = primeira_pendente()
+
+ with st.container(key="audit_steps"):
for column, (step_key, step_label) in zip(st.columns(3, gap="small"), STEP_CARDS):
with column:
ativo = st.session_state.audit_active_step == step_key
- if st.button(step_label, key=f"audit_select_{step_key}",
- type="primary" if ativo else "secondary", use_container_width=True):
+ liberada = etapa_liberada(step_key)
+ if not liberada:
+ icone_etapa = ":material/lock:"
+ elif etapa_revisada(step_key):
+ icone_etapa = ":material/check_circle:"
+ else:
+ icone_etapa = ICONES_ETAPA[step_key]
+ if st.button(step_label, key=f"audit_select_{step_key}", disabled=not liberada, icon=icone_etapa,
+ type="primary" if ativo else "secondary",
+ help=None if liberada else "Marque “Confirmo que revisei” na etapa anterior para liberar."):
st.session_state.audit_active_step = step_key
st.rerun()
@@ -592,6 +449,7 @@ def formulario() -> bool:
st.markdown('
Valores em euro (€). Renda e passivos da Estônia até 2010 '
'foram convertidos de coroas estonianas na carga.
', unsafe_allow_html=True)
audit_review_checkbox("personal")
+ botao_proxima("personal")
elif active_step == "loan":
audit_text("ID do contrato", "loan_id", max_chars=40)
@@ -604,6 +462,7 @@ def formulario() -> bool:
'Para meses após o fim da série, o modelo usa o último valor disponível. '
'Juros acima de 100% a.a. existem na base e são legítimos.
', unsafe_allow_html=True)
audit_review_checkbox("loan")
+ botao_proxima("loan")
else:
audit_number("Total de dívidas em outras instituições (€)", "liabilities_total",
@@ -614,8 +473,14 @@ def formulario() -> bool:
audit_review_checkbox("history")
st.markdown("", unsafe_allow_html=True)
- clicou = st.button("⚖️ EXECUTAR AUDITORIA DE PREÇO", key="audit_execute",
+ liberado = todas_revisadas()
+ if not liberado:
+ faltam = [d["title"] for e, d in STEP_DETAILS.items() if not etapa_revisada(e)]
+ md(f'
{icone("cadeado", 14)} A auditoria libera depois das 3 confirmações. '
+ f'Falta revisar: {esc(", ".join(faltam))}.
')
+ clicou = st.button("EXECUTAR AUDITORIA DE PREÇO", key="audit_execute", icon=":material/balance:", disabled=not liberado,
type="primary", use_container_width=True)
+ clicou = clicou and liberado # trava também no código, não só no botão
with progress_column:
with st.container(border=True):
diff --git a/tests/app/test_componentes.py b/tests/app/test_componentes.py
index 9030f13..9e5fe4d 100644
--- a/tests/app/test_componentes.py
+++ b/tests/app/test_componentes.py
@@ -125,3 +125,12 @@ def test_duas_casas_quando_os_juros_ficariam_iguais():
assert "30,56%" in cartoes[1] and "30,58%" in cartoes[2]
assert "Cobrado 30,56%" in html_barra_juros(r)
assert "24,9%" in kpis_revisao(POR_SITUACAO["acima_do_justo"])[1] # caso normal: 1 casa
+
+
+def test_icones_de_linha_sem_emoji():
+ from src.app.componentes import icone
+ svg = icone("cadeado", 14)
+ assert svg.startswith("