Commit Graph
63 Commits
Author SHA1 Message Date
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 7656f2261e docs: corrige README contraditorio sobre HTTP Basic e SESSION_SECRET
1. Frase de abertura ainda dizia 'login HTTP Basic em toda rota',
   contradizendo a secao 'Login e permissoes' mais abaixo no mesmo
   arquivo (ja correta, fala de login por sessao). Atualizada para
   'login por sessao em toda rota'.
2. A lista de variaveis de ambiente para rodar localmente (secao
   'Rodando localmente') nao mencionava SESSION_SECRET, mesmo a secao
   'Login e permissoes' ja instruindo a defini-la. Adicionada a lista,
   com nota de que e opcional em dev (chave aleatoria em memoria a cada
   start, sessoes caem a cada restart) mas deve ser fixa em producao.
3. A mencao ao ALTER TABLE FUNCIONARIO_PAINEL ADD COLUMN PERFIL estava
   em tom de fato passado. Ajustada para o mesmo padrao de aviso ja
   usado para LOG_PAINEL/FUNCIONARIO_PAINEL nesse README ('precisa ser
   rodado manualmente em producao tambem'), deixando explicito que sem
   rodar isso todo login falha com 500 (a query de autenticacao
   seleciona uma coluna PERFIL que nao existe na tabela antiga).

Sem teste automatizado - mudanca só de documentacao.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:19:09 -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 a71b990a3c docs: documenta login por sessao e perfis admin/padrao no README
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:04:37 -03:00
GustavoandClaude Sonnet 5 64990b6db2 fix: atualiza tests/test_main.py para login por sessao
tests/test_main.py nao fazia parte do mapa de arquivos do plano de
permissoes/sessao e ficou quebrado (HTTP Basic Auth) apos a troca pra
login por sessao nas tasks anteriores. Atualiza os 6 testes pra usar
cookie de sessao (app.session.criar_token) em vez de header Basic, e
ajusta a expectativa de GET / sem login (agora redireciona 303 pra
/login em vez de pedir credenciais 401).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:01:36 -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
Gustavo 9b04877d59 feat: formulario de edicao de contato na tela de detalhe do cliente
Adiciona UI (details/summary, sem JS) para editar nome/telefone/email
de um contato existente, usando a rota POST /clientes/{id}/contatos/{id}/editar
que ja existia no router e no db mas nunca tinha formulario associado.
Cobre com testes de rota (sucesso + telefone duplicado) e um teste
verificando que a tela de detalhe realmente renderiza o formulario.
2026-08-13 10:52:50 -03:00
GustavoandClaude Sonnet 5 0d839949fe fix: validacao de formulario, auth fail-closed, loop de confirmacao, hardening e docs (achados da revisao final)
- Fix 1 (critico): cliente_form.html agora tem required/type por campo
  (date/number) e o router captura DataError/IntegrityError em
  criar/editar, reexibindo o form com erro_gravacao em vez de 500.
- Fix 2 (importante): auth.py falha fechado quando ADMIN_USER/
  ADMIN_PASSWORD nao estao configurados (nao tenta mais compare_digest
  com string vazia).
- Fix 3 (importante): avisos de codigo duplicado e DV invalido agora
  carregam a flag de confirmacao um do outro adiante, quebrando o loop
  quando ambos disparam no mesmo cliente.
- Fix 4 (menor): corrige precedencia 'or 42' -> 'and 42' no lambda de
  teste de criar_cliente.
- Fix 5 (menor): desabilita /docs, /redoc, /openapi.json no FastAPI().
- Fix 6 (menor): comentario explicando o valor de Prorrogacao no fixture
  de teste de chaves.
- Fix 7 (menor): .env.example usa host.docker.internal por padrao,
  docker-compose.yml ganha extra_hosts para Linux, README.md novo com
  quickstart real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:46:23 -03:00