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>
.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>
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>
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>
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>
- --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>
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>
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>
_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>
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>
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>
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.
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).
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.
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.
- 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).
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.
- 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).
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).
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.
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.
- 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>