Commit Graph
64 Commits
Author SHA1 Message Date
Gustavo 4fd4649edd feat: coluna ATIVO em CLIENTE e filtro na camada de banco 2026-08-20 09:49:19 -03:00
Gustavo 53dcd1c9d1 feat: botao de gerar chave de reativacao por linha na lista de clientes 2026-08-20 09:31:13 -03:00
Gustavo b830ce14dc feat: passa csrf_token no contexto da listagem de clientes 2026-08-20 09:30:08 -03:00
GustavoandClaude Sonnet 5 9a7d12f941 fix: achados da revisao final (remove TAMANHO_PAGINA morto, httponly no cookie de paginacao, limpa testes)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:11:08 -03:00
GustavoandClaude Sonnet 5 9ce6bcdcad feat: seletor de linhas por pagina e paginacao numerada na listagem de clientes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:01:12 -03:00
Gustavo 5a3c4ac092 feat: logout apaga o cookie de preferencia de linhas por pagina 2026-08-20 08:55:56 -03:00
Gustavo 819c1f8e28 feat: GET /clientes le por_pagina da query/cookie e expoe contexto de paginacao 2026-08-20 08:52:17 -03:00
Gustavo 9d5702a01e feat: funcoes puras de resolucao de tamanho de pagina e janela de paginas 2026-08-20 08:48:35 -03:00
GustavoandClaude Sonnet 5 f940ff5768 feat: bloqueia CNPJ/CPF duplicado ao cadastrar ou editar cliente
Mesmo padrão já usado pro CODIGO (db_clientes.codigo_em_uso): nova
db_clientes.cnpj_em_uso(documento, ignorar_id=None) checada em
criar()/editar() antes de gravar. A comparação ignora pontuação dos dois
lados (não só normaliza o valor recebido) porque CLIENTE.CNPJ tem
formatação inconsistente em registros legados (alguns com máscara,
outros sem) — sem isso, um cadastro novo com o mesmo documento de um
legado ainda mascarado passaria batido.

app/templates/cliente_form.html: nova mensagem de erro
'erro_cnpj_duplicado', mesmo estilo visual do erro de código duplicado.

6 testes novos (unitário do db + integração do router pros dois
fluxos), suíte completa (196 testes) passando.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 09:13:43 -03:00
GustavoandClaude Sonnet 5 eed473c5f3 fix: telefone de contato sem DDI e updates/excludes que fingiam sucesso
- app/routers/contatos.py: CLIENTE_CONTATO.TELEFONE passa a ser gravado
  sempre com DDI 55 (normalizar_telefone_com_ddi, mesma normalização já
  usada em CLIENTE.TELEFONE/CELULAR) — confirmado no node '10 - Validar
  Telefones' do workflow n8n de produção (ER6Piy9yu93uOVuY) que o
  telefone é usado exatamente como está gravado, sem nenhuma
  normalização própria. Um contato cadastrado pelo painel sem o
  funcionário digitar '55' na frente nunca recebia a chave automática
  por WhatsApp — falha silenciosa, sem nenhum erro visível.

- app/db/clientes.py, app/db/contatos.py, app/db/funcionarios.py:
  excluir_cliente/atualizar_contato/excluir_contato/redefinir_senha
  passam a retornar bool (cur.rowcount > 0) em vez de None — um
  ID inexistente não gera erro nenhum do MySQL (WHERE só não bate com
  nada), então sem checar rowcount o router não tinha como saber que
  não alterou/excluiu nada.

- app/routers/clientes.py, app/routers/contatos.py,
  app/routers/funcionarios.py: editar/excluir/redefinir-senha/
  alternar-ativo passam a checar essa existência e mostrar 'não
  encontrado' em vez de registrar 'sucesso' no log e redirecionar pra
  uma tela que também vai dizer que não encontrou nada.

- app/templates/clientes_lista.html: busca não estava com urlencode nos
  links de paginação/ordenação — um termo de busca com '&' (comum em
  razão social, ex. 'Fulano & Cia') cortava o resto da query string do
  link, perdendo página/ordenação.

Suíte completa (192 testes) passando, com 13 testes novos cobrindo os
casos de 'não encontrado' e o DDI do telefone de contato.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 11:50:37 -03:00
GustavoandClaude Sonnet 5 d7a08ddd8d security: rate limit no login, cookies Secure condicional, security headers e deps com CVE
- app/rate_limit.py: bloqueia por IP após 5 tentativas de senha erradas
  em 5 minutos em POST /login (rota pré-autenticação, hoje sem nenhuma
  proteção contra força bruta).
- Cookie de sessão (routers/auth.py) e cookie CSRF (main.py) passam a
  sair com Secure quando a requisição chega em HTTPS (request.url.scheme)
  — hoje é sempre False porque produção ainda serve HTTP puro, mas o
  cookie passa a ficar protegido automaticamente assim que um TLS for
  colocado na frente, sem precisar editar isso de novo.
- main.py: novo middleware de security headers (X-Content-Type-Options,
  X-Frame-Options: DENY, Referrer-Policy, Content-Security-Policy,
  Strict-Transport-Security condicional a HTTPS).
- requirements.txt: fastapi 0.115.0 -> 0.115.6 (starlette >=0.40.0,
  corrige DoS por memória não limitada ao parsear multipart/form-data —
  GHSA-f96h-pmfr-66vw), python-multipart 0.0.9 -> 0.0.20 (várias
  vulnerabilidades de DoS corrigidas), jinja2 3.1.4 -> 3.1.6 (escapes de
  sandbox). httpx/uvicorn/PyMySQL já estavam em versões sem CVE
  conhecida — conferido via osv.dev.
- Dockerfile: container não roda mais como root.

Suíte completa (181 testes) passando, incluindo testes novos para o
rate limiter (unitários + integração via POST /login) e smoke test
manual confirmando headers, fluxo de login com CSRF e o bloqueio por
força bruta.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 11:17:41 -03:00
GustavoandClaude Sonnet 5 b0830651f4 feat(painel-clientes-web): favicon com a logomarca da GTI
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 14:23:44 -03:00
GustavoandClaude Sonnet 5 dea9dc4401 fix: corrige achados da revisao final (DDI idempotente, mascara DDI 12/13 digitos, status DV, validacao no load, autofill dispara mascaras)
Endereca 5 achados da revisao final de branch:
- Important #1: normalizar_telefone_com_ddi agora e idempotente (nao duplica 55 independente do tamanho)
- Important #2: mascara de telefone detecta DDI colado por tamanho total (12/13 digitos), nao mais so pelo campo especifico
- Minor #3: buscarCnpj pula lookup na Receita quando DV do CNPJ e invalido, preservando a mensagem de erro
- Minor #4: detectarEValidarDocumento roda uma vez no carregamento da pagina
- Minor #5: autofill da Receita dispara evento input para acionar as mascaras de UF/CEP/TELEFONE

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:31:20 -03:00
GustavoandClaude Sonnet 5 eec7ce21b5 feat: validação de CNPJ e máscaras de telefone/UF/CEP no formulário de cliente
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:21:15 -03:00
GustavoandClaude Sonnet 5 59f15d9ac0 feat: bloquear CNPJ com DV inválido, gravar telefone/UF/CEP normalizados
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:18:24 -03:00
GustavoandClaude Sonnet 5 8b5e7e251e feat: normalizar_telefone_com_ddi/normalizar_uf/normalizar_cep
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:14:39 -03:00
GustavoandClaude Sonnet 5 5c577a644a fix(seguranca): move algoritmo de geracao de chave do JS pro backend
O algoritmo (CHAVE_INTERNA + funcoes gerarReativacao/gerarInstalacao)
ficava 100% legivel no codigo-fonte da pagina — qualquer pessoa com
acesso ao painel conseguia abrir o DevTools e extrair a logica pra
gerar chaves de licenca fora do sistema.

Move tudo pra app/gerador_chave.py (roda so no servidor) e expoe via
POST /gerador-chave/gerar (autenticado, protegido por CSRF). A pagina
agora so manda os dados do formulario e mostra o resultado que volta
do backend. Algoritmo verificado byte-a-byte contra os 3 exemplos
reais conhecidos (cliente 000001, ALUGUEL). Bonus: o byte inicial
agora usa secrets.randbelow (CSPRNG) em vez de Math.random() do JS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 10:03:19 -03:00
GustavoandClaude Sonnet 5 a3a84abd37 fix: campo codigo do cliente so aceita numeros + botoes de tabela nao quebram linha
Gerador de chave: filtra caracteres nao numericos do campo codigo em
tempo real (input event), alem da validacao ja existente no submit.

Tabelas: a.botao/button ganham white-space: nowrap, corrigindo o
botao 'Abrir' (e outros) quebrando em duas linhas depois que as
celulas de dado passaram a permitir quebra de texto longo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 09:19:44 -03:00
GustavoandClaude Sonnet 5 f6cf0f2e53 fix: botao de ver senha no login ficava esticado pra largura total
.login-cartao button { width: 100%; } tinha mais especificidade CSS
que .botao-ver-senha e forcava o botao do olho a ocupar 100% da
largura do campo, jogando o icone (alinhado por padrao a esquerda no
flex) pro canto esquerdo por cima do input. Regra do width:100% agora
so mira o botao de submit; o campo de senha vira um wrapper flex
(input + botao lado a lado), mais robusto que posicionamento absoluto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 08:56:20 -03:00
GustavoandClaude Sonnet 5 1728a2dbdd fix: sidebar fixa na tela (nao sobe com tabelas longas) + mostrar/ocultar senha no login
Sidebar antes esticava junto com o conteudo (align-items:stretch do
flex), entao em telas com tabela longa (Clientes) o rodape (tema/sair/
recolher) ficava fora da area visivel sem rolar a pagina toda. Agora
fica position:sticky, presa ao topo da viewport, com rolagem propria
se a lista de links for maior que a tela.

Login ganha um botao de olho pra mostrar/ocultar a senha digitada.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 08:52:35 -03:00
GustavoandClaude Sonnet 5 7febe7c652 fix: evita rolagem horizontal nas tabelas (celulas com quebra de linha)
O white-space: nowrap em toda celula de tabela impedia o texto de
quebrar linha, entao valores longos de Razao/Fantasia/CNPJ estouravam
a largura da tabela e forcavam rolagem horizontal mesmo em telas
grandes. Cabecalhos continuam sem quebra (nomes curtos); celulas de
dado agora quebram linha quando necessario.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 08:45:11 -03:00
GustavoandClaude Sonnet 5 6aeed3af7e fix: aplica classe ativo nos links da sidebar conforme rota atual
Adiciona class condicional (ativo se request.url.path.startswith(...))
em cada link de app/templates/base.html, usando o estilo .sidebar-nav
a.ativo que ja existia em estilo.css mas nunca era aplicado.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:48:37 -03:00
GustavoandClaude Sonnet 5 9012795973 fix: corrige contraste AA do laranja de acento no tema claro
- --cor-acento-contraste passa de branco pra #17171a (texto escuro sobre
  fundo laranja em botoes, unificando com o tema escuro) - contraste ~7:1
- novo token --cor-acento-texto (#B3510C no tema claro, #F5821F no
  tema escuro) usado para laranja como cor de TEXTO sobre fundo claro
  (link padrao, badge-acento, item ativo da sidebar, chave gerada em
  gerador_chave.html) - contraste 5.12:1 contra branco, acima do
  minimo AA de 4.5:1

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:48:12 -03:00
GustavoandClaude Sonnet 5 b5098a3aed feat: redesign da tela de log (badges de status)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:38:01 -03:00
GustavoandClaude Sonnet 5 9cef2c8d17 feat: redesign de minha-conta e gerador de chave
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:35:05 -03:00
GustavoandClaude Sonnet 5 9114303507 feat: redesign da lista e formulario de funcionarios (badges de perfil/status)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:32:07 -03:00
GustavoandClaude Sonnet 5 18029de9f8 feat: redesign do detalhe de cliente (contatos e status de chaves)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 22:04:31 -03:00
GustavoandClaude Sonnet 5 343f61ade7 feat: redesign da lista e formulario de cliente (cartoes, grid, tabela responsiva)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 22:01:11 -03:00
GustavoandClaude Sonnet 5 e21b1646a7 feat: redesign da tela de login com identidade GTI
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:57:37 -03:00
GustavoandClaude Sonnet 5 310f5e1f71 feat: sistema de design (tokens claro/escuro) + sidebar responsiva em base.html
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:53:30 -03:00
GustavoandClaude Sonnet 5 e5f5d4211c fix: desativar/rebaixar funcionario nao revogava sessao ativa (ate 12h)
Perfil e status ativo vinham so do cookie assinado (session.py), entao um
funcionario desativado (ATIVO=0) ou rebaixado (ADMIN->PADRAO) continuava
com acesso total ate o cookie expirar (ate 12h) - inclusive a
/funcionarios se era admin.

exigir_login (app/auth.py) agora reconfirma contra o banco a cada
requisicao quando o login nao e o da conta de bootstrap (ADMIN_USER do
.env, que nao tem linha em FUNCIONARIO_PAINEL e continua sempre admin via
variavel de ambiente): busca db_funcionarios.obter_por_login(login); se
nao existir mais ou ATIVO != 1, levanta NaoAutenticado (forca novo
login); caso contrario, monta o UsuarioLogado com o PERFIL atual do
banco, nao o do cookie - rebaixamento tambem passa a ter efeito imediato.

Testes de regressao em tests/test_auth.py:
- test_exigir_login_autentica_funcionario_ativo
- test_exigir_login_revoga_sessao_ao_desativar_funcionario
- test_exigir_admin_usa_perfil_atual_do_banco_nao_do_cookie
- test_exigir_login_conta_bootstrap_nao_consulta_banco

Ajustou tambem test_exigir_admin_rejeita_padrao (usava um login fake sem
linha em FUNCIONARIO_PAINEL, que agora conflita com a checagem nova -
passou a usar a fixture funcionario_teste). Suite inteira roda
147 passed apos a mudanca.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:18:58 -03:00
GustavoandClaude Sonnet 5 ba8ffaede7 fix: bloqueia open-redirect via next=/\evil.com no login
_proximo_seguro() (app/routers/auth.py) so recusava next que comecasse
com "//", mas navegadores modernos tratam "/\" como equivalente a
"//" (URL protocol-relative), entao next=/\evil.com ainda era um open
redirect explorável via link de login malicioso.

Bloqueia tambem o prefixo "/\".

Teste de regressao em tests/test_router_auth.py: POST /login com
next=/\evil.com precisa redirecionar para /clientes, nao para o host
externo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:18:46 -03:00
GustavoandClaude Sonnet 5 aa1c528197 fix: botao Sair quebrado em /clientes, /log e /gerador-chave (csrf_token vazio)
base.html usava {{ csrf_token }}, variavel que so algumas rotas passam
explicitamente no contexto do template. Em /clientes (GET), /log e
/gerador-chave a variavel nunca era passada, entao o campo oculto do
formulario de logout renderizava com value="" e exigir_csrf (app/csrf.py)
rejeitava o POST /logout com 403 - o usuario nao conseguia clicar em
'Sair' logo apos logar, ja que /clientes e para onde todo login cai.

Corrige na raiz: usa {{ csrf_token or request.state.csrf_token }}, caindo
para o valor que csrf_cookie_middleware (app/main.py) ja popula em toda
requisicao quando a rota nao passar csrf_token explicitamente. Protege
tambem qualquer rota futura que esqueca de passar a variavel.

Testes de regressao em tests/test_main.py: confirma que GET /clientes
renderiza um csrf_token nao vazio no formulario de logout, e que
POST /logout funciona (303) apos visitar /clientes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:18:39 -03:00
GustavoandClaude Sonnet 5 81f730df9e feat: perfil admin/padrao em funcionarios, protege ultimo admin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:56:52 -03:00
GustavoandClaude Sonnet 5 a2a5fc7905 feat: nav com botao Sair, Minha conta e Funcionarios so para admin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:51:52 -03:00
GustavoandClaude Sonnet 5 09424e8afb feat: liga login por sessao, middleware de usuario e handler de NaoAutenticado
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:49:05 -03:00
GustavoandClaude Sonnet 5 4a5c70861d feat: tela Minha conta para trocar a propria senha
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:44:15 -03:00
GustavoandClaude Sonnet 5 c4079b3e47 feat: rotas de login/logoff (app/routers/auth.py)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:38:43 -03:00
GustavoandClaude Sonnet 5 1b80290f6f feat: login por sessao (troca HTTP Basic), UsuarioLogado, exigir_admin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:33:30 -03:00
GustavoandClaude Sonnet 5 718e845651 feat: coluna PERFIL em FUNCIONARIO_PAINEL + contar_admins_ativos
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:29:45 -03:00
GustavoandClaude Sonnet 5 95b1da7c10 feat: token de sessao assinado (app/session.py)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:25:18 -03:00
Gustavo 361c2ca770 feat: gerador de chave de instalacao (aluguel/compra) na tela /gerador-chave
Adiciona a segunda rotina recuperada do Chave.exe original: chave de
instalacao (8 caracteres tipicos), ao lado da chave de reativacao ja
existente na mesma tela, com um seletor de tipo.

Algoritmo (confirmado matematicamente contra os 3 exemplos reais
fornecidos, byte a byte):
- codigoCliente * 19 + 31 vira um numero inserido sem cifrar no meio
  da chave final.
- A modalidade ("30"=ALUGUEL / "00"=COMPRA) e cifrada com o MESMO
  algoritmo/chave interna ja usado na reativacao (soma modular + XOR),
  gerando 6 hex (3 bytes: 1 aleatorio + 2 cifrados).
- Chave final = 3 primeiros hex + valor do cliente + 3 ultimos hex.

100% client-side (mesmo js puro da reativacao), nunca grava no banco.
Verifiquei os 3 exemplos (DC45092C, 2E35083D, 3D25090C) reproduzindo
o algoritmo em Python antes de trocar o JS - bateram exatamente.

106/106 testes passando.
2026-08-13 16:08:48 -03:00
Gustavo 3158a416a9 feat: preenchimento automatico de dados do cliente a partir do CNPJ
Ao sair do campo CNPJ no formulario de cliente, busca automaticamente
na BrasilAPI (publica, gratuita, sem chave - dados abertos da Receita)
e preenche Razao/Fantasia/Endereco/Cidade/Bairro/UF/CEP/Telefone/
E-mail, so nos campos que estiverem vazios (nunca sobrescreve o que
ja foi digitado). So funciona para CNPJ - CPF e dado pessoal, sem
consulta publica, e a rota recusa (404) se detectar CPF.

- app/cnpj_lookup.py: cliente HTTP pra BrasilAPI, degrada em silencio
  (retorna None) se nao encontrar/rede cair/formato nao reconhecido
  (ex.: CNPJ alfanumerico novo, que a BrasilAPI pode nao cobrir ainda).
- Nova rota GET /clientes/consulta-cnpj/{documento}, chamada via
  fetch() no formulario (JS vanilla, sem framework).
- Testado ponta a ponta contra a API real.

106/106 testes passando (10 novos).
2026-08-13 15:05:26 -03:00
Gustavo d18a5e6e29 feat: cabecalhos clicaveis para ordenar (crescente/decrescente) a lista de clientes
Codigo, Razao, Fantasia e CNPJ agora sao links no cabecalho da tabela:
clicar ordena crescente; clicar de novo na mesma coluna alterna pra
decrescente (e vice-versa). Trocar de coluna sempre comeca em
crescente. Seta (▲/▼) indica a coluna/direcao ativa.

db_clientes.listar_clientes() ganha o parametro direcao (whitelist
asc/desc, igual ja acontecia com ordenar_por). Links de
Anterior/Proxima da paginacao preservam a direcao escolhida.

97/97 testes passando.
2026-08-13 14:37:25 -03:00
Gustavo 2f1d008404 fix: busca de clientes restrita ao campo escolhido em ordenar_por
Antes, digitar qualquer coisa na busca comparava com CODIGO, CNPJ,
RAZAO e FANTASIA ao mesmo tempo (OR), entao escolher 'Ordenar por:
Codigo' e buscar '242' trazia tambem clientes cujo CNPJ ou Razao
continham '242' em outro lugar. Agora a busca usa so a coluna
selecionada no dropdown (mesma whitelist ja usada pra ordenacao).

listar_clientes()/contar_clientes() e o template ganharam o ajuste;
o dropdown mudou de rotulo pra 'Buscar/ordenar por' pra deixar claro
que controla os dois.

93/93 testes passando.
2026-08-13 14:24:09 -03:00
Gustavo 8d83122e98 feat: login individual por funcionario (cadastro, senha com hash, ativar/desativar)
- Tabela FUNCIONARIO_PAINEL (nova, ja criada no banco de dev) +
  app/db/funcionarios.py: nome/login/senha (hash scrypt, stdlib, sem
  dependencia nova) por funcionario, nunca excluido de verdade (so
  ativo/inativo).
- app/senha.py: gerar_hash/verificar_senha com hashlib.scrypt + salt
  aleatorio por senha.
- app/auth.py: login passa a checar duas fontes, nessa ordem -
  ADMIN_USER/ADMIN_PASSWORD (bootstrap, continua exigindo as duas
  preenchidas) e depois FUNCIONARIO_PAINEL (so contas ATIVO=1). O
  nome retornado (usado no log) passa a ser o nome de verdade do
  funcionario, nao mais sempre 'admin'.
- Tela /funcionarios: listar, criar (com confirmacao de senha e
  checagem de login duplicado), editar nome/login, redefinir senha
  (form separado) e ativar/desativar. Tudo logado em LOG_PAINEL.
- Testado ponta a ponta contra o app real: funcionario criado via
  tela consegue logar de verdade, e a acao aparece no /log com o
  nome de quem fez.

93/93 testes passando (23 novos).
2026-08-13 14:19:58 -03:00
Gustavo dad1abf30b fix: parametro correto da API de rows do n8n (limit, nao take)
Testado contra a instancia real (devn8n.gtigrupo.com.br) com uma API
key de verdade: 'take' retorna 400 'Unknown query parameter'; o nome
certo e 'limit'. Confirmado end-to-end - /log agora mostra os envios
reais (ERRO_ENVIO, CLIENTE_NAO_CADASTRADO etc.) da Data Table
pagamentos_processados.
2026-08-13 14:14:40 -03:00
Gustavo 9ed0bf57c2 feat: log de acoes/envios, ordenacao/filtro, codigo sequencial, campos opcionais
- Tabela LOG_PAINEL (nova, ja criada no banco de dev) + app/db/log.py:
  toda criacao/edicao/exclusao de cliente e contato fica registrada
  (usuario, acao, sucesso/erro, detalhe). Tela /log mostra esse
  historico.
- app/n8n_client.py: leitura opcional (N8N_API_URL/N8N_API_KEY) da
  Data Table pagamentos_processados do n8n, mostrando os envios
  automaticos de chave na mesma tela /log. Degrada de forma segura
  (lista vazia) se nao configurado ou se a chamada falhar.
- Lista de clientes ganha ordenacao configuravel (codigo/razao/
  fantasia/CNPJ) via whitelist, alem da busca que ja existia.
- CODIGO duplicado agora BLOQUEIA de verdade na criacao/edicao (sem
  bypass) — antes so avisava com opcao de confirmar mesmo assim.
- Formulario de novo cliente sugere o proximo CODIGO sequencial
  (editavel), ignorando a faixa reservada para dados de teste
  (>= 900000) para nao ser contaminado por sobra de teste no banco.
- IMPLANTACAO/MENSALIDADE/JUROS_MORA/JUROS_MULTA/DATA_CONTRATO/
  VENCIMENTO_CONTRATO deixam de ser obrigatorios — em branco, gravam
  default (0,00 / hoje) em vez de bloquear o cadastro. Só CODIGO/CNPJ/
  RAZAO/FANTASIA continuam obrigatorios.
- Limpeza: removidas 10 linhas de teste vazadas (CODIGO 999902) que
  ficaram no banco de dev por sessoes de teste interrompidas.

69/69 testes passando (16 novos).
2026-08-13 14:04:24 -03:00
Gustavo bb8fd596b1 feat: protecao CSRF (double-submit cookie) e paginacao da lista de clientes
CSRF: novo app/csrf.py + middleware em app/main.py garantem um cookie
csrf_token por navegador; todo formulario que muda estado (criar/editar/
excluir cliente, adicionar/editar/excluir contato) embute esse valor
como campo oculto, e Depends(exigir_csrf) confere no POST que o valor
enviado bate com o cookie antes de deixar a rota executar. Verificado
ponta a ponta com o app real rodando (curl): token errado -> 403,
token correto -> 303.

Paginacao: db_clientes.listar_clientes ganha offset, nova
contar_clientes(); GET /clientes aceita ?pagina=N (100 clientes por
pagina), template mostra Anterior/Proxima e o total.

53/53 testes passando (8 novos).
2026-08-13 11:02:17 -03:00
Gustavo 96cbd8a294 fix: normalizar CNPJ/telefone de CLIENTE antes de gravar (sem mascara)
CLIENTE.CNPJ/TELEFONE/CELULAR agora sao gravados sempre sem pontuacao
(so digitos, e letras no caso do CNPJ alfanumerico), igual ao que
CLIENTE_CONTATO.TELEFONE ja fazia. Resolve a inconsistencia de formato
apontada na revisao final do plano (achado 7): duas tabelas do mesmo
app guardando telefone/documento em formatos diferentes.

So afeta escritas novas feitas pelo painel (criar/editar cliente) -
registros legados existentes so ficam sem mascara quando forem
reeditados e salvos aqui; nao faz backfill em massa dos dados atuais.
2026-08-13 10:54:37 -03:00