Add 'gerenciador/' from commit '34cc9c75a57dedbf61a6e6a1ba1f35f76e72434b'
git-subtree-dir: gerenciador git-subtree-mainline:f4f546c928git-subtree-split:34cc9c75a5
This commit is contained in:
@@ -0,0 +1,698 @@
|
||||
# GTI Gerenciador — Discovery Técnico (v1)
|
||||
|
||||
> **Nota crítica de escopo, apurada nesta missão**: "GTI Gerenciador" é o nome
|
||||
> comercial/de produção do repositório `painel-clientes-web`. Não existe um
|
||||
> projeto separado com esse nome — confirmado com o usuário após uma busca
|
||||
> read-only que não encontrou nenhuma pasta `*gerenciador*` em `D:\PROJETOS`
|
||||
> nem em `10.0.10.117`. **O sistema real é um painel de gestão de
|
||||
> clientes/licenciamento** (cadastro de clientes, funcionários, geração de
|
||||
> chave de reativação, log de auditoria) — **não** um gerenciador de frota/RMM
|
||||
> com Agents Windows, heartbeat, jobs e blockers, como o briefing da missão
|
||||
> presumia. As seções abaixo refletem o sistema real; onde o briefing pedia
|
||||
> algo que não existe (ex.: módulo de Agents), isso é dito explicitamente, não
|
||||
> inventado.
|
||||
|
||||
## 1. Identidade do repositório
|
||||
|
||||
```
|
||||
PROJECT_NAME=painel-clientes-web ("GTI Gerenciador" em produção)
|
||||
REPOSITORY_PATH=D:\PROJETOS\BLOQUEIO SGV\painel-clientes-web
|
||||
CURRENT_BRANCH=master
|
||||
HEAD_COMMIT=ce718f7ac22a583fd8c5d3a23415a19dcfc31291 (2026-08-20)
|
||||
WORKING_TREE_STATUS=limpo (só ../.claude/ não rastreado, fora do repo)
|
||||
GIT_REMOTE=nenhum configurado — repositório só local, nunca teve push
|
||||
```
|
||||
|
||||
Estrutura principal (`app/`):
|
||||
- `routers/`: auth, clientes, contatos, funcionarios, log, minha_conta, chave_manual
|
||||
- `db/`: clientes, contatos, funcionarios, log, chaves, connection — SQL cru via PyMySQL, sem ORM
|
||||
- `templates/`: Jinja2 server-rendered (base.html + uma página por rota)
|
||||
- `static/`: CSS puro + JS vanilla inline
|
||||
- `auth.py`, `csrf.py`, `session.py`, `senha.py`, `rate_limit.py`, `n8n_client.py`, `gerador_chave.py`, `cnpj_lookup.py`
|
||||
- `docs/`, `docs/superpowers/specs|plans` — specs e planos de cada feature (fluxo de desenvolvimento assistido por IA usado neste projeto)
|
||||
|
||||
Não há pasta `frontend/`, `migrations/` (Alembic) ou `deployment/` separada — é
|
||||
um monólito único, com deploy manual (não há `migrations/`; mudanças de schema
|
||||
são `ALTER TABLE` manuais documentadas em docstring + README).
|
||||
|
||||
## 2. Stack da aplicação
|
||||
|
||||
```
|
||||
BACKEND_FRAMEWORK=FastAPI
|
||||
BACKEND_LANGUAGE=Python
|
||||
BACKEND_VERSION=3.12 (imagem Docker python:3.12-slim)
|
||||
|
||||
FRONTEND_TYPE=server-rendered
|
||||
FRONTEND_FRAMEWORK=Jinja2Templates (FastAPI)
|
||||
FRONTEND_BUILD_CHAIN=nenhuma (sem bundler/npm — CSS/JS estáticos escritos à mão)
|
||||
|
||||
DATABASE=MySQL 5.7
|
||||
ORM=nenhum (PyMySQL cru, SQL parametrizado manualmente)
|
||||
MIGRATION_TOOL=nenhum (ALTER TABLE manual, documentado em docstring do módulo + README.md)
|
||||
|
||||
CACHE=nenhum
|
||||
QUEUE=nenhuma
|
||||
BACKGROUND_JOBS=nenhum (tudo síncrono, request/response)
|
||||
|
||||
WEBSERVER=uvicorn (ASGI, standalone — sem gunicorn na frente)
|
||||
APPLICATION_SERVER=uvicorn[standard]==0.30.6
|
||||
```
|
||||
|
||||
Dependências (`requirements.txt`, todas pinadas por auditoria de segurança de
|
||||
2026-08-15): `fastapi==0.115.6`, `uvicorn[standard]==0.30.6`, `jinja2==3.1.6`,
|
||||
`python-multipart==0.0.20`, `PyMySQL==1.1.1`, `python-dotenv==1.0.1`,
|
||||
`httpx==0.27.2` (usado só para chamar a API do n8n).
|
||||
|
||||
**Classificação**: monolito único, server-rendered, sem frontend separado.
|
||||
|
||||
## 3. Funcionalidades atuais
|
||||
|
||||
| MODULE_NAME | PURPOSE | MAIN_ROUTES | MAIN_TABLES | DEPENDENCIES |
|
||||
|---|---|---|---|---|
|
||||
| Clientes | CRUD de clientes (CNPJ, contato, status ativo/inativo) | `/clientes`, `/clientes/{id}`, `/clientes/{id}/alternar-ativo` | `CLIENTE` | `cnpj_lookup.py` (consulta CNPJ externa) |
|
||||
| Contatos | Contatos por cliente (nome/telefone/e-mail) | rotas aninhadas sob `/clientes/{id}` | `CLIENTE_CONTATO` | — |
|
||||
| Funcionários | Usuários do painel (login/senha/perfil ADMIN\|PADRAO) | `/funcionarios` | `FUNCIONARIO_PAINEL` | `senha.py` (scrypt) |
|
||||
| Gerador de chave | Gera chave de reativação/liberação (cifra própria) | `/gerador-chave`, `/gerador-chave/gerar` | — (sem persistência própria) | `gerador_chave.py` |
|
||||
| Log | Auditoria de ações do painel + espelho de envios automáticos do n8n | `/log`, `/log/limpar` (admin) | `LOG_PAINEL` | `n8n_client.py` (lê Data Table do n8n via API, não escreve) |
|
||||
| Autenticação | Login humano (admin de ambiente + funcionários) | `/login`, `/logout` | `FUNCIONARIO_PAINEL` + `ADMIN_USER`/`ADMIN_PASSWORD` (env) | `session.py`, `csrf.py` |
|
||||
| Minha conta | Trocar a própria senha | `/minha-conta` | `FUNCIONARIO_PAINEL` | — |
|
||||
|
||||
**Não existe**: módulo de servidores, agentes, backups, monitoramento,
|
||||
inventário ou "integrações" no sentido do briefing original — o sistema é
|
||||
puramente sobre clientes/licenciamento, não sobre gestão de infraestrutura de
|
||||
terceiros.
|
||||
|
||||
## 4. Autenticação e autorização
|
||||
|
||||
```
|
||||
AUTH_MODEL=cookie de sessão assinado por HMAC-SHA256 (chave própria em session.py,
|
||||
não é JWT de biblioteca) + cookie CSRF de double-submit
|
||||
USER_TABLE=FUNCIONARIO_PAINEL (login, hash de senha, PERFIL)
|
||||
ROLE_MODEL=dois perfis fixos: ADMIN e PADRAO (sem granularidade por permissão)
|
||||
PERMISSION_MODEL=checagem direta de perfil nas rotas (Depends(exigir_admin) vs
|
||||
Depends(exigir_login)) — não é RBAC configurável
|
||||
```
|
||||
|
||||
- Existe também um **login "raiz" fora da tabela**: usuário/senha vindos de
|
||||
`ADMIN_USER`/`ADMIN_PASSWORD` (variáveis de ambiente), comparação direta —
|
||||
serve de acesso de emergência independente do banco.
|
||||
- Senha de funcionário: `scrypt` (stdlib do Python, sem bcrypt/argon2/passlib).
|
||||
- Sessão: HMAC-SHA256 sobre payload com `login`, `nome`, `perfil`, `exp` —
|
||||
**validade de 12 horas**, sem refresh automático (expira, força novo login).
|
||||
- CSRF: cookie `csrf_token` (HttpOnly) + campo hidden em todo formulário POST,
|
||||
comparados no servidor (double-submit).
|
||||
- Logout: limpa cookie de sessão.
|
||||
|
||||
**Reuso por GTI Fiscal**: o modelo de sessão (HMAC próprio) é simples e
|
||||
replicável, mas está fortemente amarrado a este processo Python específico
|
||||
(chave HMAC em memória do processo/env, sem endpoint de validação exposto
|
||||
para outro serviço consumir). Reuso real exigiria **extrair isso para um
|
||||
serviço de auth compartilhado** ou aceitar duplicar a lógica — não há hoje um
|
||||
"SSO" ou endpoint `/auth/validar` pronto para outro backend chamar.
|
||||
|
||||
## 5. Banco de dados
|
||||
|
||||
```
|
||||
DB_ENGINE=MySQL
|
||||
DB_VERSION=5.7 (imagem Docker mysql:5.7)
|
||||
DB_HOST_MODEL=container único (gtisoftw-mysql), sem replicação/cluster
|
||||
DB_NAME=gtisoftw_clientes_dev (nome de dev usado também em produção — dívida
|
||||
histórica de nomenclatura, não um ambiente de teste real)
|
||||
MIGRATION_HEAD=n/a (sem Alembic/ferramenta — ALTER TABLE manual documentado)
|
||||
```
|
||||
|
||||
Tabelas principais conhecidas: `CLIENTE`, `CLIENTE_CONTATO`,
|
||||
`FUNCIONARIO_PAINEL`, `LOG_PAINEL` (+ tabelas do sistema legado
|
||||
`licenciamento-n8n` que compartilham o mesmo banco, fora do escopo direto do
|
||||
painel). Total de ~9 tabelas segundo o dump de referência mais recente
|
||||
(`Banco para Prod/README-DEPLOY-PROXMOX.md`).
|
||||
|
||||
- Volume de dados: pequeno — ~209 clientes, 127 contatos (dump de referência).
|
||||
- Pool/connections: sem pool — `PyMySQL` abre e fecha conexão por request
|
||||
(`get_connection()`/`conn.close()` a cada operação), aceitável no volume
|
||||
atual mas não escalaria bem sob concorrência alta.
|
||||
- Backup: **não encontrei rotina de backup automatizada** no servidor (sem
|
||||
cron de dump, sem script de backup do container) — só existe o
|
||||
procedimento manual documentado em `Banco para Prod/README-DEPLOY-PROXMOX.md`
|
||||
para popular/restaurar a partir de um dump.
|
||||
- Persistência: volume Docker nomeado (`gtisoftw_mysql_data`, implícito pelo
|
||||
compose de `Banco para Prod/`), não bind-mount.
|
||||
|
||||
```
|
||||
CAN_GTI_FISCAL_SHARE_DATABASE=requires_analysis
|
||||
```
|
||||
|
||||
| Opção | Vantagens | Riscos |
|
||||
|---|---|---|
|
||||
| A) Mesmo PostgreSQL, schemas separados | Simples de administrar (1 backup, 1 instância), sem novo processo de banco | GTI Fiscal quer **PostgreSQL**, este ambiente só roda **MySQL 5.7** — não é "mesmo Postgres", seria instalar um segundo motor de banco do zero |
|
||||
| B) Bancos lógicos separados no mesmo motor | N/A aqui pelo mesmo motivo acima | — |
|
||||
| C) PostgreSQL separado no mesmo host | Cada stack usa o motor que já foi desenhado para ela, sem gambiarra de schema | Mais um processo residente numa VM que já está com RAM apertada (ver §22) |
|
||||
|
||||
Dado que GTI Fiscal foi desenhado para PostgreSQL e este ambiente só tem
|
||||
MySQL, a opção realista é **C) PostgreSQL próprio no mesmo host** (se houver
|
||||
recursos — ver §22) — não há cenário realista de "compartilhar" o banco atual
|
||||
entre os dois sistemas.
|
||||
|
||||
## 6. Ambiente de execução
|
||||
|
||||
```
|
||||
HOSTNAME=srvbd
|
||||
OS=Ubuntu 24.04.4 LTS
|
||||
KERNEL=Linux 6.8.0-137-generic
|
||||
|
||||
VIRTUALIZATION_VISIBLE=kvm (systemd-detect-virt)
|
||||
CONTAINER_OR_VM=VM (QEMU/KVM — "Standard PC (i440FX + PIIX, 1996)", vendor QEMU)
|
||||
```
|
||||
|
||||
Confirmado como **VM dentro de um host Proxmox**: a pasta local
|
||||
`Banco para Prod/README-DEPLOY-PROXMOX.md` (deste mesmo repositório) documenta
|
||||
explicitamente o processo de deploy do banco "na VM/LXC do Proxmox" — este
|
||||
servidor (10.0.10.117) é essa VM.
|
||||
|
||||
```
|
||||
CPU_CORES=2
|
||||
RAM_TOTAL=1.9 GiB
|
||||
RAM_AVAILABLE=~1.2 GiB (742 MiB em uso, 1.0 GiB em buff/cache)
|
||||
DISK_TOTAL=8.1 GiB (partição raiz)
|
||||
DISK_FREE=1.4 GiB (82% em uso)
|
||||
|
||||
LOAD=0.08, 0.02, 0.01 (ociosa)
|
||||
UPTIME=36 dias
|
||||
```
|
||||
|
||||
Só leitura — nenhum comando de escrita executado.
|
||||
|
||||
## 7. Docker / containers
|
||||
|
||||
```
|
||||
DOCKER_VERSION=29.7.2
|
||||
COMPOSE_VERSION=v5.4.0
|
||||
```
|
||||
|
||||
| NAME | IMAGE | STATUS | PORTS | NETWORKS |
|
||||
|---|---|---|---|---|
|
||||
| `painel-clientes-web-painel-1` | `painel-clientes-web-painel` (build local) | Up 4 semanas | `0.0.0.0:80->8000/tcp` | `default` + `bancoparaprod_default` (externa) |
|
||||
| `gtisoftw-mysql` | `mysql:5.7` | Up 5 semanas | `0.0.0.0:3306->3306/tcp` | `bancoparaprod_default` (dedução — mesmo network externo usado pelo painel) |
|
||||
|
||||
`docker-compose.yml` do painel (em `/home/administrador/painel-clientes-web/`):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
painel:
|
||||
build: .
|
||||
ports: ["80:8000"]
|
||||
env_file: [.env]
|
||||
networks: [default, bancoparaprod_default]
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
bancoparaprod_default:
|
||||
external: true
|
||||
```
|
||||
|
||||
O container do MySQL vem de um **compose separado** (`Banco para Prod/`,
|
||||
network `bancoparaprod_default` criada por ele, consumida como externa pelo
|
||||
painel) — os dois projetos/composes são independentes, unidos só pela network
|
||||
Docker compartilhada. Sem healthcheck explícito em nenhum dos dois serviços.
|
||||
|
||||
## 8. Recursos usados atualmente
|
||||
|
||||
```
|
||||
painel-clientes-web-painel-1: CPU 0.12% RAM 78.8 MiB
|
||||
gtisoftw-mysql: CPU 0.07% RAM 199.4 MiB
|
||||
```
|
||||
|
||||
```
|
||||
CURRENT_RESOURCE_PRESSURE=baixa em CPU (2 vCPU quase ociosos); MODERADA em RAM
|
||||
(só a folga não-cache é ~200-400MiB reais depois
|
||||
de reservar o que o SO/buffers já usam) e ALTA em
|
||||
disco (82% da partição raiz de 8GB já ocupado)
|
||||
HEADROOM_ESTIMATE=CPU: folgado. RAM: ~1.2GiB "available" reportado pelo
|
||||
kernel, mas isso conta cache reclamável — para uma
|
||||
segunda aplicação Python + segundo banco (Postgres) a
|
||||
margem real é apertada, não confortável. Disco: crítico
|
||||
— só 1.4GiB livres, qualquer novo serviço com imagem
|
||||
Docker própria (Postgres + Hub FastAPI) provavelmente
|
||||
não cabe sem aumentar o disco da VM antes.
|
||||
```
|
||||
|
||||
## 9. Portas e rede
|
||||
|
||||
| SERVICE | LISTEN_ADDRESS | PORT | EXPOSED_EXTERNALLY | PURPOSE |
|
||||
|---|---|---|---|---|
|
||||
| painel-clientes-web (uvicorn) | 0.0.0.0 | 80 | sim (direto, sem proxy na frente) | HTTP da aplicação |
|
||||
| MySQL | 0.0.0.0 | 3306 | **sim** | Banco relacional |
|
||||
| SSH | 0.0.0.0 | 22 | sim | Administração |
|
||||
| systemd-resolved | 127.0.0.53/127.0.0.54 | 53 | não | DNS local |
|
||||
|
||||
Nenhuma porta foi aberta ou alterada — apenas listadas via `ss -tlnp`.
|
||||
|
||||
## 10. Acesso web
|
||||
|
||||
```
|
||||
CURRENT_URL=http://10.0.10.117/ (IP direto, porta 80)
|
||||
DOMAIN=nenhum domínio público configurado
|
||||
HTTPS=não (confirma pendência já registrada na auditoria de segurança de
|
||||
2026-08-15: "falta de TLS em produção é pendência aceita
|
||||
conscientemente")
|
||||
TLS_PROVIDER=n/a
|
||||
```
|
||||
|
||||
Fluxo real: `Internet/rede interna → 10.0.10.117:80 → uvicorn (direto)`. Não
|
||||
há DNS/Cloudflare nem qualquer camada intermediária.
|
||||
|
||||
## 11. Reverse proxy
|
||||
|
||||
```
|
||||
REVERSE_PROXY_TYPE=NENHUM instalado (nginx/caddy/traefik ausentes no host)
|
||||
REVERSE_PROXY_LOCATION=n/a
|
||||
TLS_TERMINATION=n/a
|
||||
PROXY_TARGET=n/a
|
||||
```
|
||||
|
||||
Isso é uma lacuna relevante para a integração: hoje **não existe** onde
|
||||
configurar `/fiscal` ou `fiscal.dominio` — seria preciso instalar um reverse
|
||||
proxy do zero (Caddy é o mais barato em RAM para esta VM apertada) antes de
|
||||
sequer cogitar rotear dois serviços por hostname/porta 80/443 únicos.
|
||||
|
||||
## 12. Firewall e exposição
|
||||
|
||||
```
|
||||
PUBLICLY_EXPOSED_PORTS=22 (SSH), 80 (aplicação), 3306 (MySQL)
|
||||
```
|
||||
|
||||
**MySQL está exposto publicamente** (`0.0.0.0:3306`), não só em loopback/rede
|
||||
interna — isso já era uma pendência independente desta missão, mas relevante
|
||||
para decidir se um segundo banco (Postgres do GTI Fiscal) repete o mesmo
|
||||
padrão ou é mantido só em rede interna Docker. Não alterei nenhuma regra.
|
||||
|
||||
## 13. Backup
|
||||
|
||||
```
|
||||
BACKUP_STATUS=inexistente como rotina automatizada
|
||||
RESTORE_PROCEDURE=manual, documentado (Banco para Prod/README-DEPLOY-PROXMOX.md)
|
||||
— restaura a partir de dump SQL versionado no repo local,
|
||||
não a partir de um backup do estado de produção atual
|
||||
LAST_BACKUP_IF_VISIBLE=não há evidência de nenhum backup de produção
|
||||
rodado — dumps encontrados localmente são snapshots
|
||||
pontuais feitos manualmente (ex.:
|
||||
dump-gtisoftw_clientes_dev-20260806_104136.sql)
|
||||
```
|
||||
|
||||
## 14. Logs / observabilidade
|
||||
|
||||
```
|
||||
LOGGING_MODEL=stdout do container (docker logs), mais LOG_PAINEL (tabela de
|
||||
auditoria de ações do painel, não logs técnicos)
|
||||
MONITORING_MODEL=nenhum (sem Prometheus/Grafana/Loki/Zabbix — nada instalado
|
||||
no host além do sysstat padrão do Ubuntu)
|
||||
```
|
||||
|
||||
Eventos do GTI Fiscal (heartbeat, jobs, blockers) **não têm onde se integrar**
|
||||
hoje — não existe stack de observabilidade centralizada nesta VM para
|
||||
"encaixar" nele.
|
||||
|
||||
## 15. Serviços do sistema
|
||||
|
||||
- `systemd`: só serviços padrão do Ubuntu + Docker daemon. Nenhum serviço
|
||||
systemd customizado para o painel (ele roda via `restart: unless-stopped`
|
||||
do próprio Docker, não via unit systemd).
|
||||
- `cron`: só as entradas padrão do Ubuntu (`cron.hourly/daily/weekly/monthly`
|
||||
via anacron) — nenhum job customizado do projeto.
|
||||
- Sem Windows services (host é Linux).
|
||||
|
||||
## 16. Deploy atual
|
||||
|
||||
```
|
||||
DEPLOY_PROCEDURE=manual, via script Python ad-hoc (paramiko/SFTP) rodado pelo
|
||||
desenvolvedor: empacota o repo local em .tar.gz (excluindo
|
||||
docker-compose.yml e .env de produção), sobe por SFTP,
|
||||
extrai por cima do diretório existente na VM, roda
|
||||
"docker compose build" (com retry manual em caso de timeout
|
||||
do registro Docker Hub) e "docker compose up -d". Migração
|
||||
de schema (quando há) é um ALTER TABLE manual rodado à parte,
|
||||
documentado no docstring do módulo Python afetado.
|
||||
ROLLBACK_PROCEDURE=NÃO documentado/automatizado — não há tag de imagem
|
||||
anterior preservada nem processo de reverter; reverter
|
||||
exigiria checkout de um commit anterior local e repetir
|
||||
o deploy manual.
|
||||
```
|
||||
|
||||
Não há CI/CD, GitHub Actions, ou pipeline — é 100% manual, disparado por uma
|
||||
pessoa a cada mudança.
|
||||
|
||||
## 17. Estrutura do frontend
|
||||
|
||||
- **Layout**: `app/templates/base.html` — sidebar fixa com navegação (ícones
|
||||
SVG inline), área de conteúdo à direita, tema claro/escuro via
|
||||
`localStorage` + atributo `data-tema` no `<html>`.
|
||||
- **Templates**: um arquivo Jinja2 por página, todos estendendo `base.html`
|
||||
via `{% block conteudo %}`. Sem componentização (sem includes/macros
|
||||
reutilizáveis além do layout base).
|
||||
- **CSS**: um único `app/static/estilo.css`, variáveis CSS para cores
|
||||
(tema claro/escuro), sem framework (sem Tailwind/Bootstrap).
|
||||
- **Identidade visual**: logo GTI (`logo-gti-256x256.png`), paleta escura por
|
||||
padrão com acento laranja — a UI já é **rotulada "GTI"** na sidebar
|
||||
("Painel de Clientes").
|
||||
- **Auth nos templates**: `request.state.usuario` (setado por middleware)
|
||||
controla o que aparece na sidebar (ex.: "Funcionários" só para admin).
|
||||
|
||||
```
|
||||
CAN_ADD_FISCAL_MENU=sim, tecnicamente simples — é só adicionar um <a> na
|
||||
sidebar de base.html com a mesma checagem de
|
||||
request.state.usuario.eh_admin, IF o Fiscal virar rota
|
||||
deste mesmo processo FastAPI (Opção A da seção 20)
|
||||
CAN_REUSE_LAYOUT=sim, mas exigiria portar o CSS de tema claro/escuro e a
|
||||
sidebar para dentro do stack do GTI Fiscal (que hoje
|
||||
também é Jinja2 — compatível em princípio) SE a integração
|
||||
for Opção C (backend separado, UI incorporada)
|
||||
CAN_REUSE_AUTH=parcialmente — ver §4: o modelo é simples de replicar/portar,
|
||||
mas não existe hoje um serviço de auth desacoplado para
|
||||
"importar" sem duplicar código
|
||||
```
|
||||
|
||||
## 18. APIs e integrações
|
||||
|
||||
Inventário de rotas por módulo (ver §3). Não há uma "API" no sentido REST
|
||||
externo — todas as rotas retornam HTML (exceto `/gerador-chave/gerar`, que
|
||||
retorna JSON, chamado via `fetch()` do próprio frontend do painel). Não há
|
||||
autenticação por token/Bearer em lugar nenhum — só cookie de sessão.
|
||||
|
||||
Integração externa existente: `app/n8n_client.py` — chama a API REST de uma
|
||||
instância n8n (`N8N_API_URL`/`N8N_API_KEY` via env) para **ler** (não
|
||||
escrever) uma Data Table de envios automáticos de chave, exibida em `/log`.
|
||||
É a única integração "tipo API" que este sistema tem.
|
||||
|
||||
**Não existe** conceito de `client`/`server`/`installation`/`agent` no
|
||||
sentido do GTI Fiscal — o mais próximo é `CLIENTE` (empresa/licenciada), que
|
||||
é uma entidade de **negócio/cobrança**, não de infraestrutura técnica (não
|
||||
tem hostname, IP, SO, ou qualquer atributo de "servidor monitorado").
|
||||
|
||||
## 19. Modelo de cliente/servidor
|
||||
|
||||
```
|
||||
CLIENTE = empresa licenciada (CODIGO, RAZAO, FANTASIA, CNPJ, ATIVO,
|
||||
DIA_VENCIMENTO) — chave primária ID (int), CODIGO é o
|
||||
identificador de negócio usado no gerador de chave
|
||||
CLIENTE_CONTATO = contatos humanos daquele cliente (nome/telefone/e-mail),
|
||||
FK CLIENTE_ID
|
||||
```
|
||||
|
||||
Não existem `EMPRESA` (é o mesmo que `CLIENTE`), `SERVIDOR`, `INSTALAÇÃO`,
|
||||
nem `AGENT` neste sistema.
|
||||
|
||||
Comparando com o GTI Fiscal (`tenant/client`, `installation_id`, `agent_id`,
|
||||
`hostname`, contexto de empresa): a única sobreposição conceitual real é
|
||||
**cliente/empresa** — `CLIENTE.CNPJ` poderia, em teoria, ser a chave de
|
||||
correlação entre "quem é o cliente" nos dois sistemas (mesmo CNPJ = mesma
|
||||
empresa), mas isso exigiria um design explícito de correlação (ex.: o Fiscal
|
||||
guardar o `CLIENTE_ID` ou `CODIGO` deste sistema como referência externa) —
|
||||
**não existe isso hoje**, seria trabalho novo de integração, não algo pronto
|
||||
para reaproveitar.
|
||||
|
||||
## 20. Possível integração do GTI Fiscal
|
||||
|
||||
**Opção A — Mesmo processo/aplicação** (GTI Fiscal como módulo interno do
|
||||
FastAPI existente): exigiria portar o Fiscal de FastAPI+PostgreSQL+Alembic
|
||||
próprios para dentro deste monolito Python+MySQL+SQL-cru — descartaria a
|
||||
stack já pronta do Fiscal (Postgres, migrations, auth de Agent via Bearer).
|
||||
Alto retrabalho, alto acoplamento, alto risco de regressão no painel de
|
||||
clientes hoje estável. **Não recomendado.**
|
||||
|
||||
**Opção B — Serviço separado na mesma máquina** (dois processos FastAPI
|
||||
independentes, dois bancos, roteados por porta ou subdomínio): mais fiel à
|
||||
arquitetura já pronta do Fiscal (Postgres, migrations Alembic, auth de Agent
|
||||
por Bearer token — nada disso precisa mudar). Exige: instalar PostgreSQL
|
||||
nesta VM, instalar um reverse proxy (inexistente hoje — §11), e — dado o
|
||||
disco em 82% de uso (§22) — **provavelmente aumentar o disco da VM antes**.
|
||||
|
||||
**Opção C — Backend Fiscal separado + UI incorporada no Gerenciador**: o
|
||||
Fiscal mantém seu backend/API de Agents isolado (bom para segurança — Agents
|
||||
externos batendo Bearer token não precisam tocar no mesmo processo do
|
||||
painel), mas a tela humana ("Resumo", "Agents/Servidores", "Blockers",
|
||||
"Jobs") é servida a partir do processo do painel, chamando o backend do
|
||||
Fiscal internamente (rede Docker) e renderizando com o mesmo layout Jinja2 já
|
||||
existente (§17). Combina isolamento de falha (Fiscal fora do ar não derruba
|
||||
o painel de clientes) com a experiência visual unificada que o usuário quer.
|
||||
|
||||
## 21. Recomendação
|
||||
|
||||
**Opção B para o backend (times/dados isolados) combinada com a fachada de UI
|
||||
da Opção C**, na prática: dois processos FastAPI + dois bancos (MySQL do
|
||||
painel, PostgreSQL do Fiscal) na mesma VM/rede Docker, sem misturar código
|
||||
Python nem schemas — e, quando fizer sentido visualmente, o painel ganha um
|
||||
item de menu "Fiscal" que ou (a) faz proxy/iframe para o serviço do Fiscal
|
||||
rodando noutra porta/subpath via reverse proxy, ou (b) mais elegante: o
|
||||
próprio Hub FastAPI do Fiscal serve suas páginas Jinja2 reaproveitando o CSS
|
||||
do painel (arquivo estático compartilhado ou copiado), ficando visualmente
|
||||
idêntico sem juntar os dois processos.
|
||||
|
||||
Critérios que pesaram:
|
||||
- **Baixa manutenção / isolamento de falhas**: times/schemas separados
|
||||
evitam que um bug do Fiscal (ex.: processamento de 500k produtos) afete o
|
||||
painel de clientes em produção, que hoje é estável.
|
||||
- **Segurança**: Agents Windows externos autenticam via Bearer contra o
|
||||
backend do Fiscal — não faz sentido esse tráfego externo passar pelo mesmo
|
||||
processo/sessão-cookie do painel administrativo interno.
|
||||
- **Reaproveitamento real disponível**: só a **máquina** (VM), a **rede
|
||||
Docker** e, superficialmente, o **CSS/layout** são reaproveitáveis hoje —
|
||||
banco, auth e modelo de dados **não são**, então forçar Opção A jogaria
|
||||
fora o trabalho já feito na stack do Fiscal sem ganho real.
|
||||
- **Pré-requisito bloqueante**: nada disso cabe sem antes resolver o disco
|
||||
(82% cheio) e decidir sobre reverse proxy — ver §22-23.
|
||||
|
||||
## 22. Capacidade da máquina
|
||||
|
||||
```
|
||||
CAN_HOST_GTI_FISCAL_ON_SAME_MACHINE=false, no estado atual da VM (sem
|
||||
aumentar disco/RAM primeiro)
|
||||
```
|
||||
|
||||
Considerando a carga alvo do briefing (Hub FastAPI + PostgreSQL + 100 Agents
|
||||
iniciais + 500k produtos + event logs + blockers + jobs) contra o que esta VM
|
||||
tem hoje:
|
||||
|
||||
- **CPU**: 2 vCPU, carga atual quase zero — folga real, não é o gargalo.
|
||||
- **RAM**: 1.9GiB total, dos quais ~940MiB já em uso por SO+containers
|
||||
atuais. PostgreSQL sozinho, com um volume de 500k produtos + índices, tende
|
||||
a querer bem mais que a folga restante para não sofrer com cache pequeno
|
||||
(`shared_buffers`/`effective_cache_size` ficariam artificialmente baixos).
|
||||
100 Agents com heartbeat periódico é tráfego leve por si, mas soma-se ao
|
||||
processo do Hub. **Risco real de troca/swap sob carga.**
|
||||
- **Disco**: 8.1GiB total, **1.4GiB livres (82% usado)**. Uma imagem
|
||||
PostgreSQL + o próprio volume de dados (500k produtos, mesmo que
|
||||
compactos, mais índices, mais WAL) muito provavelmente **não cabe** nesse
|
||||
espaço livre atual, ainda mais somado à imagem Docker do Hub FastAPI. Este
|
||||
é o **gargalo mais crítico e imediato**, antes mesmo de RAM.
|
||||
|
||||
```
|
||||
NEEDS_PROXMOX_RESOURCE_INFO=true
|
||||
```
|
||||
|
||||
O que falta, especificamente (só visível a partir do host Proxmox, não de
|
||||
dentro da VM):
|
||||
1. Quanto de CPU/RAM/disco físico está livre no host Proxmox para **aumentar**
|
||||
esta VM (ou criar uma segunda) — de dentro da VM não dá pra saber se o
|
||||
host tem folga para um `pvesm resize`/`qm resize` do disco de 8→20GB+, por
|
||||
exemplo.
|
||||
2. Política de backup/snapshot do Proxmox para esta VM (indiretamente
|
||||
relevante para §13 — se o Proxmox já faz snapshot no nível de
|
||||
hypervisor, isso muda a análise de "backup inexistente").
|
||||
3. Se há outras VMs/LXCs no mesmo host Proxmox concorrendo pelos mesmos
|
||||
recursos físicos (I/O de disco compartilhado principalmente).
|
||||
|
||||
## 23. Conflitos potenciais
|
||||
|
||||
```
|
||||
PORT_CONFLICTS=nenhum imediato — Fiscal pode usar outra porta (8001, por
|
||||
exemplo); porta 80 já está tomada pelo painel
|
||||
DATABASE_CONFLICTS=nenhum — motores diferentes (MySQL vs PostgreSQL) não
|
||||
colidem, mas cada um consome memória/disco próprios
|
||||
NETWORK_CONFLICTS=nenhum — network Docker externa (bancoparaprod_default)
|
||||
pode ser reaproveitada ou uma nova criada, sem colisão
|
||||
DOMAIN_CONFLICTS=n/a — não há domínio configurado hoje para nenhum dos dois
|
||||
DEPENDENCY_CONFLICTS=nenhum a nível de processo (processos Python isolados
|
||||
em containers separados não compartilham
|
||||
dependências) — mas concorrem por RAM/disco da
|
||||
mesma VM (ver §22)
|
||||
AUTH_CONFLICTS=nenhum tecnicamente (mecanismos diferentes, cookie de sessão
|
||||
vs Bearer de Agent, não colidem) — mas também não se somam:
|
||||
um usuário logado no painel não fica "logado" automaticamente
|
||||
no Fiscal sem um esforço explícito de integração de auth
|
||||
```
|
||||
|
||||
## 24. O que pode ser reutilizado
|
||||
|
||||
```
|
||||
REUSABLE_COMPONENTS=
|
||||
- Máquina/VM (10.0.10.117) — há folga de CPU, mas não de disco (ver §22)
|
||||
- Rede Docker (bancoparaprod_default ou nova network dedicada)
|
||||
- CSS/identidade visual (app/static/estilo.css + logo GTI) — copiável para
|
||||
o Fiscal se ele também for Jinja2
|
||||
- Padrão de deploy manual via SFTP/docker compose (mesmo processo humano já
|
||||
usado, sem precisar inventar um novo fluxo de deploy)
|
||||
- Conceito de correlação por CNPJ entre CLIENTE (aqui) e tenant/client (lá),
|
||||
SE for desenhado explicitamente — não existe pronto
|
||||
```
|
||||
|
||||
## 25. O que deve ficar separado
|
||||
|
||||
```
|
||||
SHOULD_REMAIN_ISOLATED=
|
||||
- Banco de dados (MySQL do painel vs PostgreSQL do Fiscal — motores
|
||||
diferentes, sem cenário real de compartilhar)
|
||||
- Autenticação de Agent (Bearer token) — não deve passar pelo mesmo
|
||||
middleware de sessão-cookie do painel administrativo
|
||||
- Migrations (Alembic do Fiscal vs ALTER TABLE manual do painel — misturar
|
||||
processos de schema é fonte garantida de erro operacional)
|
||||
- Processo do Hub FastAPI do Fiscal — mantê-lo como processo próprio
|
||||
(Opção B/C) preserva o isolamento de falha que o painel de clientes tem
|
||||
hoje (37 dias de uptime sem intervenção)
|
||||
```
|
||||
|
||||
## 26. Segurança
|
||||
|
||||
Achados relevantes (todos já visíveis publicamente ou já documentados em
|
||||
auditoria anterior deste mesmo projeto — nada novo exposto aqui):
|
||||
|
||||
- **MySQL exposto publicamente** na porta 3306 (`0.0.0.0`), não restrito a
|
||||
rede interna/loopback — ver §12.
|
||||
- **Sem TLS/HTTPS** em produção — tráfego HTTP puro na porta 80, incluindo
|
||||
login (pendência já registrada conscientemente em memória de projeto
|
||||
anterior — não uma novidade desta auditoria).
|
||||
- `.env` de produção existe e não foi lido (só variáveis específicas
|
||||
necessárias, via `grep` pontual, nunca o arquivo inteiro) — nenhuma senha,
|
||||
token ou secret aparece neste relatório.
|
||||
- Auth de funcionário usa `scrypt` (adequado); auth "raiz" via
|
||||
`ADMIN_USER`/`ADMIN_PASSWORD` em variável de ambiente, comparação direta
|
||||
(sem hashing) — aceitável para uma única conta de emergência, mas vale
|
||||
nota para quem for desenhar auth compartilhada com o Fiscal.
|
||||
- Nenhuma correção foi feita — só relatado, como pedido.
|
||||
|
||||
## 27. Artefato
|
||||
|
||||
Este arquivo: `docs/GTI_GERENCIADOR_DISCOVERY_V1.md`, dentro do repositório
|
||||
`painel-clientes-web`. **Não commitado** — a missão pediu regime read-only;
|
||||
o arquivo fica no working tree local para o usuário revisar e decidir se
|
||||
versiona.
|
||||
|
||||
## 28. Confirmação — nada foi alterado
|
||||
|
||||
Nenhum código, banco, container, proxy, firewall ou configuração foi
|
||||
modificado nesta missão. Todos os comandos no servidor foram de leitura
|
||||
(`hostnamectl`, `uptime`, `free`, `df`, `docker ps`, `docker stats`, `ss`,
|
||||
`systemctl list-units`, `find`, `ls`, `cat` de arquivos não sensíveis,
|
||||
`crontab -l`). A única tentativa que exigiria credencial adicional (contar
|
||||
tabelas via `mysql -uroot`) falhou por falta de acesso e não foi insistida.
|
||||
|
||||
---
|
||||
|
||||
## Relatório final (resumo estruturado)
|
||||
|
||||
```
|
||||
STATUS=discovery concluído, read-only, nenhuma alteração feita
|
||||
|
||||
PROJECT_NAME=painel-clientes-web ("GTI Gerenciador" é o nome comercial —
|
||||
confirmado com o usuário, não é um projeto separado)
|
||||
REPOSITORY_PATH=D:\PROJETOS\BLOQUEIO SGV\painel-clientes-web
|
||||
BRANCH=master
|
||||
HEAD=ce718f7ac22a583fd8c5d3a23415a19dcfc31291
|
||||
WORKTREE=limpo
|
||||
|
||||
BACKEND=FastAPI (Python 3.12) + PyMySQL, sem ORM
|
||||
FRONTEND=Jinja2 server-rendered, sem build chain
|
||||
DATABASE=MySQL 5.7 (gtisoftw_clientes_dev), sem migrations formais
|
||||
AUTH_MODEL=cookie de sessão HMAC-SHA256 próprio (12h) + CSRF double-submit;
|
||||
login de funcionário (FUNCIONARIO_PAINEL, scrypt) + login raiz
|
||||
via env
|
||||
|
||||
HOSTNAME=srvbd
|
||||
OS=Ubuntu 24.04.4 LTS
|
||||
VIRTUALIZATION=KVM (VM dentro de Proxmox, confirmado por doc local do projeto)
|
||||
|
||||
CPU=2 vCPU (ocioso)
|
||||
RAM=1.9GiB total (~940MiB em uso)
|
||||
DISK_FREE=1.4GiB de 8.1GiB (82% usado — CRÍTICO)
|
||||
|
||||
DOCKER=29.7.2 / Compose v5.4.0
|
||||
CONTAINERS=painel-clientes-web-painel-1 (up 4 semanas), gtisoftw-mysql (up 5 semanas)
|
||||
|
||||
CURRENT_URL=http://10.0.10.117/ (IP direto, sem domínio)
|
||||
DOMAIN=nenhum
|
||||
HTTPS=não
|
||||
REVERSE_PROXY=nenhum instalado
|
||||
|
||||
PUBLIC_PORTS=22 (SSH), 80 (app), 3306 (MySQL — exposto publicamente)
|
||||
|
||||
BACKUP=inexistente como rotina automatizada; só restore manual documentado
|
||||
MONITORING=inexistente (sem Prometheus/Grafana/Loki)
|
||||
|
||||
CLIENT_MODEL=CLIENTE (empresa licenciada, negócio/cobrança) — sem conceito
|
||||
de servidor/instalação/agente técnico
|
||||
SERVER_MODEL=não existe
|
||||
AGENT_MODEL=não existe
|
||||
|
||||
CAN_REUSE_AUTH=parcial (modelo simples, mas não desacoplado/exposto para
|
||||
outro serviço consumir sem duplicar código)
|
||||
CAN_REUSE_CLIENT_MODEL=parcial (CNPJ como chave de correlação possível,
|
||||
exigiria design novo de integração)
|
||||
CAN_REUSE_SERVER_MODEL=n/a (não existe)
|
||||
CAN_REUSE_LAYOUT=sim (CSS/Jinja2 portável se o Fiscal também for Jinja2)
|
||||
|
||||
CAN_HOST_GTI_FISCAL_ON_SAME_MACHINE=false no estado atual (disco em 82%,
|
||||
RAM apertada) — viável DEPOIS de
|
||||
aumentar disco (e provavelmente RAM)
|
||||
da VM
|
||||
NEEDS_PROXMOX_RESOURCE_INFO=true (capacidade física do host Proxmox para
|
||||
redimensionar esta VM; política de
|
||||
backup/snapshot no nível de hypervisor; outras
|
||||
VMs concorrendo pelo mesmo host)
|
||||
|
||||
RECOMMENDED_INTEGRATION_MODE=B (backend separado, mesma VM/rede Docker) +
|
||||
fachada visual estilo C (menu "Fiscal" no
|
||||
painel, reaproveitando layout/CSS)
|
||||
RECOMMENDATION_REASON=isolamento de falha e de auth (Agents Bearer não devem
|
||||
passar pelo processo/sessão do painel), motores de
|
||||
banco incompatíveis (MySQL vs Postgres) inviabilizam
|
||||
Opção A, e só máquina+rede+CSS são reaproveitáveis de
|
||||
verdade hoje
|
||||
|
||||
PORT_CONFLICTS=nenhum
|
||||
DATABASE_CONFLICTS=nenhum (motores diferentes)
|
||||
AUTH_CONFLICTS=nenhum tecnicamente, mas também nenhuma integração pronta
|
||||
|
||||
REUSABLE_COMPONENTS=VM/rede Docker, CSS/identidade visual, processo de
|
||||
deploy manual já estabelecido
|
||||
SHOULD_REMAIN_ISOLATED=banco de dados, auth de Agent, migrations, processo
|
||||
do Hub FastAPI do Fiscal
|
||||
|
||||
GTI_FISCAL_INTEGRATION_PREREQUISITES=
|
||||
1. Aumentar disco da VM (crítico — 82% usado hoje, não cabe Postgres+dados)
|
||||
2. Confirmar/aumentar RAM disponível junto ao Proxmox
|
||||
3. Instalar reverse proxy (Caddy é o mais leve) — hoje não existe nenhum
|
||||
4. Decidir domínio/subdomínio (nenhum configurado hoje)
|
||||
5. Desenhar (não implementar ainda) a correlação CLIENTE.CNPJ ↔ tenant do
|
||||
Fiscal, se a intenção for cruzar dados de cliente entre os dois sistemas
|
||||
6. Resolver a exposição pública do MySQL (3306) antes de repetir o padrão
|
||||
com um segundo banco
|
||||
|
||||
RISKS=
|
||||
- Disco cheio (82%) é bloqueante técnico imediato, não é só "atenção"
|
||||
- MySQL público na 3306 é uma exposição já existente, independente desta
|
||||
missão, mas relevante para não repetir o padrão
|
||||
- Sem backup automatizado hoje — somar um segundo banco sem resolver isso
|
||||
primeiro dobra a superfície de risco de perda de dados
|
||||
- Sem TLS — Agents do Fiscal batendo HTTPS externo precisarão de reverse
|
||||
proxy com certificado, que hoje não existe nesta VM
|
||||
|
||||
NEXT_STEP=levantar capacidade real do host Proxmox (item bloqueante
|
||||
NEEDS_PROXMOX_RESOURCE_INFO) e decidir se a VM atual cresce ou se
|
||||
uma segunda VM é criada especificamente para o Fiscal — só depois
|
||||
disso faz sentido detalhar a Opção B/C escolhida em um plano de
|
||||
implementação
|
||||
|
||||
VERDICT=Integração é viável e recomendável na forma B+C (backend separado,
|
||||
UI visualmente unificada), mas NÃO deve começar antes de resolver
|
||||
disco/RAM da VM e decidir sobre reverse proxy — do contrário, o
|
||||
Fiscal nasce competindo por recursos com um painel de clientes que
|
||||
hoje está estável e em produção real.
|
||||
```
|
||||
@@ -0,0 +1,235 @@
|
||||
# Log: Seções Escondidas por Padrão — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Fazer as duas seções da tela `/log` ("Ações no painel" e "Envios
|
||||
automáticos de chave (n8n)") começarem fechadas ao carregar a página,
|
||||
exigindo um clique no título ou em um botão "Mostrar" para expandir.
|
||||
|
||||
**Architecture:** Envolver cada seção em `<details class="secao-log">`
|
||||
nativo do HTML (fechado por padrão, sem `open`), com um `<summary>` que
|
||||
contém o título e um botão `Mostrar`/`Ocultar`. Um script inline pequeno
|
||||
sincroniza o texto do botão e `aria-expanded` com o evento `toggle` nativo
|
||||
do `<details>` — a abertura/fechamento em si não precisa de JavaScript,
|
||||
só o texto do botão.
|
||||
|
||||
**Tech Stack:** Jinja2 (`app/templates/log.html`), CSS puro
|
||||
(`app/static/estilo.css`), JavaScript vanilla inline, pytest +
|
||||
`fastapi.testclient.TestClient` (`tests/test_router_log.py`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Nenhuma seção pode abrir automaticamente ao carregar a página (spec:
|
||||
"Fora de escopo" — abrir automaticamente qualquer seção por padrão).
|
||||
- Estado aberto/fechado não é persistido entre recarregamentos (sem
|
||||
localStorage, cookie ou parâmetro de URL).
|
||||
- Nenhum dado exibido, paginação, limite de 20 registros em "Ações no
|
||||
painel" ou o botão "Limpar log" (visível só para admin) pode mudar de
|
||||
comportamento — só o wrapper de exibição muda.
|
||||
- A ordem das seções continua: "Ações no painel" primeiro, "Envios
|
||||
automáticos de chave (n8n)" embaixo.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Seções `<details>` no template + estilo + teste
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/templates/log.html`
|
||||
- Modify: `app/static/estilo.css`
|
||||
- Test: `tests/test_router_log.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: contexto já passado pela rota `GET /log` em
|
||||
`app/routers/log.py` (`acoes`, `envios`, `envios_configurado`, `eh_admin`,
|
||||
`csrf_token`, `mensagem`, `mensagem_tipo`) — nenhuma mudança nesse router
|
||||
nesta tarefa.
|
||||
- Produces: nada consumido por outras tasks (task única).
|
||||
|
||||
- [ ] **Step 1: Escrever o teste que falha**
|
||||
|
||||
Adicionar ao final de `tests/test_router_log.py`:
|
||||
|
||||
```python
|
||||
def test_pagina_log_secoes_comecam_fechadas(monkeypatch):
|
||||
monkeypatch.setattr(router_log.db_log, "listar", lambda limite=20: [])
|
||||
monkeypatch.setattr(router_log.n8n_client, "listar_envios", lambda limite=50: [])
|
||||
monkeypatch.setattr(router_log.n8n_client, "configurado", lambda: False)
|
||||
|
||||
resposta = client.get("/log")
|
||||
assert resposta.status_code == 200
|
||||
texto = resposta.text
|
||||
assert texto.count('<details class="secao-log">') == 2
|
||||
assert "<details class=\"secao-log\" open>" not in texto
|
||||
assert "<details class='secao-log' open>" not in texto
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rodar o teste para confirmar que falha**
|
||||
|
||||
Run: `pytest tests/test_router_log.py::test_pagina_log_secoes_comecam_fechadas -v`
|
||||
Expected: FAIL — o HTML atual não tem `<details class="secao-log">`.
|
||||
|
||||
- [ ] **Step 3: Reescrever `app/templates/log.html`**
|
||||
|
||||
Substituir o conteúdo do bloco `{% block conteudo %}` inteiro por:
|
||||
|
||||
```html
|
||||
{% block conteudo %}
|
||||
<h1>Log</h1>
|
||||
|
||||
<details class="secao-log">
|
||||
<summary>
|
||||
<span class="secao-log-titulo">Ações no painel</span>
|
||||
<button type="button" class="botao-toggle-log" aria-expanded="false">Mostrar</button>
|
||||
</summary>
|
||||
<div class="secao-log-conteudo">
|
||||
{% if eh_admin %}
|
||||
<form method="post" action="/log/limpar" style="margin-bottom:14px">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" onclick="return confirm('Limpar todo o histórico de ações? Isso não pode ser desfeito.')">Limpar log</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if acoes %}
|
||||
<div class="tabela-wrap">
|
||||
<table>
|
||||
<tr><th>Quando</th><th>Usuário</th><th>Ação</th><th>Entidade</th><th>Status</th><th>Detalhe</th></tr>
|
||||
{% for a in acoes %}
|
||||
<tr>
|
||||
<td>{{ a.CRIADO_EM }}</td>
|
||||
<td>{{ a.USUARIO }}</td>
|
||||
<td>{{ a.ACAO }}</td>
|
||||
<td>{{ a.ENTIDADE }}{% if a.ENTIDADE_ID %} #{{ a.ENTIDADE_ID }}{% endif %}</td>
|
||||
<td><span class="badge {{ 'badge-sucesso' if a.SUCESSO else 'badge-neutro' }}">{{ "OK" if a.SUCESSO else "ERRO" }}</span></td>
|
||||
<td>{{ a.DETALHE or "" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Nenhuma ação registrada ainda.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="secao-log">
|
||||
<summary>
|
||||
<span class="secao-log-titulo">Envios automáticos de chave (n8n)</span>
|
||||
<button type="button" class="botao-toggle-log" aria-expanded="false">Mostrar</button>
|
||||
</summary>
|
||||
<div class="secao-log-conteudo">
|
||||
{% if not envios_configurado %}
|
||||
<div class="cartao">
|
||||
<p>
|
||||
Integração com o n8n não configurada — defina <code>N8N_API_URL</code> e
|
||||
<code>N8N_API_KEY</code> no ambiente (ver <code>.env.example</code>) para ver
|
||||
aqui os envios automáticos de chave feitos pelo workflow do n8n.
|
||||
</p>
|
||||
</div>
|
||||
{% elif not envios %}
|
||||
<p>Nenhum envio encontrado (ou não foi possível consultar o n8n agora).</p>
|
||||
{% else %}
|
||||
<div class="tabela-wrap">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Data</th><th>Cliente</th><th>Código</th><th>Competência</th><th>Status</th>
|
||||
<th>Telefones enviados</th><th>Telefones com erro</th>
|
||||
</tr>
|
||||
{% for e in envios %}
|
||||
<tr>
|
||||
<td>{{ e.data_processamento }}</td>
|
||||
<td>{{ e.nome_cliente }}</td>
|
||||
<td>{{ e.codigo_cliente }}</td>
|
||||
<td>{{ e.competencia }}</td>
|
||||
<td>{{ e.status }}</td>
|
||||
<td>{{ e.telefones_enviados }}</td>
|
||||
<td>{{ e.telefones_com_erro or "" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll(".secao-log").forEach(function (secao) {
|
||||
var botao = secao.querySelector(".botao-toggle-log");
|
||||
var atualizar = function () {
|
||||
var aberto = secao.hasAttribute("open");
|
||||
botao.textContent = aberto ? "Ocultar" : "Mostrar";
|
||||
botao.setAttribute("aria-expanded", aberto ? "true" : "false");
|
||||
};
|
||||
secao.addEventListener("toggle", atualizar);
|
||||
atualizar();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Notas importantes:
|
||||
- O `<button>` fica dentro do `<summary>`: clicar nele já aciona o toggle
|
||||
nativo do `<details>` (o clique borbulha até o `<summary>`), então não
|
||||
precisa de `onclick` para abrir/fechar — só o listener do evento
|
||||
`toggle` para trocar o texto.
|
||||
- Nenhum `<details>` tem o atributo `open` — as duas começam fechadas.
|
||||
- O botão "Limpar log" (dentro de "Ações no painel") continua exatamente
|
||||
igual, só mudou de lugar (dentro do `<div class="secao-log-conteudo">`).
|
||||
|
||||
- [ ] **Step 4: Adicionar estilo em `app/static/estilo.css`**
|
||||
|
||||
Adicionar ao final do arquivo:
|
||||
|
||||
```css
|
||||
.secao-log { margin-bottom: 20px; }
|
||||
.secao-log summary {
|
||||
display: flex; align-items: center; gap: 10px; cursor: pointer;
|
||||
list-style: none; padding: 10px 0; user-select: none;
|
||||
}
|
||||
.secao-log summary::-webkit-details-marker { display: none; }
|
||||
.secao-log summary::before {
|
||||
content: "▸"; display: inline-block; color: var(--cor-texto-secundario);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.secao-log[open] summary::before { transform: rotate(90deg); }
|
||||
.secao-log-titulo { font-size: 1.17em; font-weight: 700; }
|
||||
.botao-toggle-log {
|
||||
margin-top: 0; padding: 6px 12px; font-size: 0.85rem; font-weight: 400;
|
||||
background: transparent; border: 1px solid var(--cor-borda); color: var(--cor-texto);
|
||||
}
|
||||
.botao-toggle-log:hover { border-color: var(--cor-acento); color: var(--cor-acento); }
|
||||
.secao-log-conteudo { padding-top: 4px; }
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Rodar o teste para confirmar que passa**
|
||||
|
||||
Run: `pytest tests/test_router_log.py -v`
|
||||
Expected: PASS em todos os testes do arquivo (incluindo os 6 já existentes
|
||||
e o novo).
|
||||
|
||||
- [ ] **Step 6: Rodar a suíte completa**
|
||||
|
||||
Run: `pytest`
|
||||
Expected: PASS em todos os testes do projeto (nenhum outro teste referencia
|
||||
a estrutura HTML de `log.html`, então nada mais deve quebrar).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add app/templates/log.html app/static/estilo.css tests/test_router_log.py
|
||||
git commit -m "feat: secoes da tela de log comecam fechadas, com clique para abrir"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** as duas seções ficam em `<details>` fechado (spec:
|
||||
"estrutura"); título clicável com seta (spec: "controles — título");
|
||||
botão Mostrar/Ocultar dentro do summary, toggle nativo sem duplicar lógica
|
||||
(spec: "controles — botão"); nenhum estado persistido (spec: "estado não
|
||||
é persistido"); ordem mantida; nenhuma mudança em dados/paginação/limite/
|
||||
botão limpar (spec: "sem mudança de comportamento"). Teste cobre que as
|
||||
duas seções carregam sem `open`.
|
||||
- **Placeholder scan:** nenhum "TBD"/"implementar depois" — todo HTML, CSS
|
||||
e JS estão completos e prontos para colar.
|
||||
- **Type consistency:** não há funções/assinaturas cruzando tasks (task
|
||||
única, sem mudança de router).
|
||||
@@ -0,0 +1,249 @@
|
||||
# Tabela de Clientes UI/UX Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Ajustar largura de colunas, alinhamento/centralização e densidade
|
||||
das linhas da tabela em `/clientes`, igual para os dois perfis de usuário,
|
||||
sem alterar comportamento, dados ou permissões.
|
||||
|
||||
**Architecture:** Adicionar uma classe `table-clientes` só nessa tabela
|
||||
(escopando o CSS pra não afetar outras telas), colunas com largura e
|
||||
alinhamento fixos via CSS (`nth-child` ou classes por coluna), padding
|
||||
vertical reduzido, e uma classe de botão mais compacta pras ações da linha.
|
||||
|
||||
**Tech Stack:** Jinja2 (`app/templates/clientes_lista.html`), CSS puro
|
||||
(`app/static/estilo.css`), pytest + `fastapi.testclient.TestClient`
|
||||
(`tests/test_router_clientes.py`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Nenhuma mudança de permissão/visibilidade por perfil (admin vs. padrão)
|
||||
nesta tela — confirmado explicitamente fora de escopo.
|
||||
- Nenhuma mudança em dados exibidos, busca, filtro de status, ordenação,
|
||||
paginação, no toggle de status ou no botão "Gerar chave" — só a
|
||||
apresentação visual muda.
|
||||
- Os ajustes de CSS ficam restritos à classe `table-clientes` — não alterar
|
||||
as regras genéricas `table`/`th`/`td` (usadas por `/funcionarios`,
|
||||
`/log`, etc.).
|
||||
- Larguras/alinhamento por coluna (da spec): Código centralizado/estreito,
|
||||
Razão e Fantasia flexíveis à esquerda (dividindo o espaço igualmente),
|
||||
CNPJ centralizado/fixo sem quebrar linha, Status centralizado/estreito,
|
||||
Ações à direita.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Classe de escopo, larguras/alinhamento e densidade
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/templates/clientes_lista.html`
|
||||
- Modify: `app/static/estilo.css`
|
||||
- Test: `tests/test_router_clientes.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: contexto já passado pela rota `GET /clientes` em
|
||||
`app/routers/clientes.py` (`clientes`, `ordenar_por`, `direcao`, `status`,
|
||||
`busca`, `csrf_token`, `redirecionar_para`, paginação) — nenhuma mudança
|
||||
nesse router nesta tarefa.
|
||||
- Produces: nada consumido por outras tasks (task única).
|
||||
|
||||
- [ ] **Step 1: Escrever o teste que falha**
|
||||
|
||||
Adicionar ao final de `tests/test_router_clientes.py`:
|
||||
|
||||
```python
|
||||
def test_listar_clientes_tabela_tem_classe_de_escopo():
|
||||
resposta = client.get("/clientes")
|
||||
assert resposta.status_code == 200
|
||||
assert '<table class="table-clientes">' in resposta.text
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rodar o teste para confirmar que falha**
|
||||
|
||||
Run: `pytest tests/test_router_clientes.py::test_listar_clientes_tabela_tem_classe_de_escopo -v`
|
||||
Expected: FAIL — a tabela hoje é `<table>` sem classe.
|
||||
|
||||
- [ ] **Step 3: Editar `app/templates/clientes_lista.html`**
|
||||
|
||||
No bloco da tabela (linhas atuais 28-71), aplicar a classe `table-clientes`
|
||||
na tag `<table>` e alinhar os `<th>`/`<td>` com classes de coluna. Substituir
|
||||
o trecho:
|
||||
|
||||
```html
|
||||
<div class="tabela-wrap">
|
||||
<table>
|
||||
<tr>
|
||||
{% set colunas_tabela = [("codigo", "Código"), ("razao", "Razão"), ("fantasia", "Fantasia"), ("cnpj", "CNPJ")] %}
|
||||
{% for campo, rotulo in colunas_tabela %}
|
||||
{% set nova_direcao = "desc" if (ordenar_por == campo and direcao == "asc") else "asc" %}
|
||||
<th>
|
||||
<a href="/clientes?ordenar_por={{ campo }}&direcao={{ nova_direcao }}&pagina=1&por_pagina={{ por_pagina }}&status={{ status }}{{ busca_qs }}">
|
||||
{{ rotulo }}{% if ordenar_por == campo %}{{ " ▲" if direcao == "asc" else " ▼" }}{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for c in clientes %}
|
||||
<tr>
|
||||
<td>{{ c.CODIGO }}</td>
|
||||
<td>{{ c.RAZAO }}</td>
|
||||
<td>{{ c.FANTASIA }}</td>
|
||||
<td>{{ c.CNPJ }}</td>
|
||||
<td>
|
||||
<form method="post" action="/clientes/{{ c.ID }}/alternar-ativo" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="redirecionar_para" value="{{ redirecionar_para }}">
|
||||
<button
|
||||
type="submit"
|
||||
class="status-dot {{ 'status-dot-ativo' if c.ATIVO else 'status-dot-inativo' }}"
|
||||
title="{{ 'Ativo — clique para desativar' if c.ATIVO else 'Inativo — clique para ativar' }}"
|
||||
onclick='return confirm({{ ((("Desativar" if c.ATIVO else "Reativar") + " o cliente " + c.RAZAO + "?")) | tojson }})'
|
||||
>
|
||||
<span class="sr-only">{{ "Ativo" if c.ATIVO else "Inativo" }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<a class="botao" href="/clientes/{{ c.ID }}">Abrir</a>
|
||||
<button type="button" class="botao botao-gerar-chave" data-codigo="{{ c.CODIGO }}" onclick="gerarChaveReativacao(this)">Gerar chave</button>
|
||||
<span class="chave-feedback" aria-live="polite"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
```
|
||||
|
||||
por:
|
||||
|
||||
```html
|
||||
<div class="tabela-wrap">
|
||||
<table class="table-clientes">
|
||||
<tr>
|
||||
{% set colunas_tabela = [("codigo", "Código", "col-codigo"), ("razao", "Razão", "col-razao"), ("fantasia", "Fantasia", "col-fantasia"), ("cnpj", "CNPJ", "col-cnpj")] %}
|
||||
{% for campo, rotulo, classe_coluna in colunas_tabela %}
|
||||
{% set nova_direcao = "desc" if (ordenar_por == campo and direcao == "asc") else "asc" %}
|
||||
<th class="{{ classe_coluna }}">
|
||||
<a href="/clientes?ordenar_por={{ campo }}&direcao={{ nova_direcao }}&pagina=1&por_pagina={{ por_pagina }}&status={{ status }}{{ busca_qs }}">
|
||||
{{ rotulo }}{% if ordenar_por == campo %}{{ " ▲" if direcao == "asc" else " ▼" }}{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th class="col-status">Status</th>
|
||||
<th class="col-acoes"></th>
|
||||
</tr>
|
||||
{% for c in clientes %}
|
||||
<tr>
|
||||
<td class="col-codigo">{{ c.CODIGO }}</td>
|
||||
<td class="col-razao">{{ c.RAZAO }}</td>
|
||||
<td class="col-fantasia">{{ c.FANTASIA }}</td>
|
||||
<td class="col-cnpj">{{ c.CNPJ }}</td>
|
||||
<td class="col-status">
|
||||
<form method="post" action="/clientes/{{ c.ID }}/alternar-ativo" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="redirecionar_para" value="{{ redirecionar_para }}">
|
||||
<button
|
||||
type="submit"
|
||||
class="status-dot {{ 'status-dot-ativo' if c.ATIVO else 'status-dot-inativo' }}"
|
||||
title="{{ 'Ativo — clique para desativar' if c.ATIVO else 'Inativo — clique para ativar' }}"
|
||||
onclick='return confirm({{ ((("Desativar" if c.ATIVO else "Reativar") + " o cliente " + c.RAZAO + "?")) | tojson }})'
|
||||
>
|
||||
<span class="sr-only">{{ "Ativo" if c.ATIVO else "Inativo" }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="col-acoes">
|
||||
<a class="botao botao-tabela" href="/clientes/{{ c.ID }}">Abrir</a>
|
||||
<button type="button" class="botao botao-tabela botao-gerar-chave" data-codigo="{{ c.CODIGO }}" onclick="gerarChaveReativacao(this)">Gerar chave</button>
|
||||
<span class="chave-feedback" aria-live="polite"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
```
|
||||
|
||||
Notas:
|
||||
- `colunas_tabela` ganhou um terceiro elemento (a classe da coluna) — o
|
||||
`{% for %}` foi ajustado para desempacotar os 3 valores.
|
||||
- `botao-tabela` foi adicionado junto de `botao` nos dois botões de ação
|
||||
(não substitui `botao`, soma-se a ele — `botao-gerar-chave` continua
|
||||
presente também).
|
||||
|
||||
- [ ] **Step 4: Adicionar CSS em `app/static/estilo.css`**
|
||||
|
||||
Adicionar ao final do arquivo:
|
||||
|
||||
```css
|
||||
.table-clientes th.col-codigo, .table-clientes td.col-codigo,
|
||||
.table-clientes th.col-cnpj, .table-clientes td.col-cnpj,
|
||||
.table-clientes th.col-status, .table-clientes td.col-status {
|
||||
text-align: center;
|
||||
}
|
||||
.table-clientes th.col-acoes, .table-clientes td.col-acoes { text-align: right; }
|
||||
|
||||
.table-clientes td.col-codigo, .table-clientes td.col-cnpj {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.table-clientes td.col-cnpj { white-space: nowrap; }
|
||||
|
||||
.table-clientes col.col-codigo { width: 90px; }
|
||||
.table-clientes col.col-razao, .table-clientes col.col-fantasia { width: 27%; }
|
||||
.table-clientes col.col-cnpj { width: 160px; }
|
||||
.table-clientes col.col-status { width: 70px; }
|
||||
.table-clientes col.col-acoes { width: 230px; }
|
||||
|
||||
.table-clientes th, .table-clientes td { padding: 7px 14px; }
|
||||
|
||||
.botao-tabela { margin-top: 0; padding: 7px 12px; }
|
||||
```
|
||||
|
||||
E, no `<table class="table-clientes">` do template (Step 3), adicionar um
|
||||
`<colgroup>` logo após a abertura da tag, antes da primeira `<tr>`, para
|
||||
que as larguras acima sejam aplicadas:
|
||||
|
||||
```html
|
||||
<table class="table-clientes">
|
||||
<colgroup>
|
||||
<col class="col-codigo">
|
||||
<col class="col-razao">
|
||||
<col class="col-fantasia">
|
||||
<col class="col-cnpj">
|
||||
<col class="col-status">
|
||||
<col class="col-acoes">
|
||||
</colgroup>
|
||||
<tr>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Rodar o teste para confirmar que passa**
|
||||
|
||||
Run: `pytest tests/test_router_clientes.py -v`
|
||||
Expected: PASS em todos os testes do arquivo (existentes + o novo).
|
||||
|
||||
- [ ] **Step 6: Rodar a suíte completa**
|
||||
|
||||
Run: `pytest`
|
||||
Expected: PASS em todos os testes do projeto.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add app/templates/clientes_lista.html app/static/estilo.css tests/test_router_clientes.py
|
||||
git commit -m "feat: ajusta colunas, alinhamento e densidade da tabela de clientes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** classe de escopo `table-clientes` (spec: "escopo do
|
||||
CSS"); larguras/alinhamento por coluna via `<colgroup>` + classes (spec:
|
||||
tabela de larguras/alinhamento); cabeçalhos alinhados junto com as
|
||||
colunas (spec: "cabeçalhos"); padding `10px→7px` e `.botao-tabela` sem
|
||||
margem superior (spec: "densidade"); nenhuma mudança de dado/ordenação/
|
||||
paginação/permissão (spec: "sem mudança de comportamento" e "fora de
|
||||
escopo"). Teste cobre a classe de escopo aplicada.
|
||||
- **Placeholder scan:** nenhum "TBD"/"implementar depois" — HTML e CSS
|
||||
completos, prontos pra colar.
|
||||
- **Type consistency:** task única, sem assinaturas cruzando tasks.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Log: seções escondidas por padrão, com clique para abrir
|
||||
|
||||
## Contexto
|
||||
|
||||
A tela `/log` mostra duas tabelas: "Ações no painel" (histórico de ações do
|
||||
painel, já limitado aos 20 registros mais recentes) e "Envios automáticos de
|
||||
chave (n8n)" (log dos envios automáticos de chave via WhatsApp, sem limite).
|
||||
Como ambas aparecem sempre expandidas, a tela fica grande e atrapalha quando
|
||||
o usuário só quer olhar rapidamente um dos dois logs.
|
||||
|
||||
## Objetivo
|
||||
|
||||
Cada seção da tela `/log` deve começar **escondida** (fechada) ao carregar a
|
||||
página, exigindo um clique para expandir. Isso facilita, por exemplo, olhar
|
||||
só o log de envios de chave sem precisar rolar past o log inteiro do painel.
|
||||
|
||||
## Design
|
||||
|
||||
**Estrutura:** cada seção (`Ações no painel` e `Envios automáticos de chave
|
||||
(n8n)`) é envolvida em um elemento `<details class="secao-log">` HTML
|
||||
nativo, **fechado por padrão** (sem o atributo `open`). A ordem das seções
|
||||
não muda: Ações no painel primeiro, Envios automáticos de chave embaixo.
|
||||
|
||||
**Controles (dois, redundantes, ambos alternam o mesmo estado):**
|
||||
- O título da seção (dentro de `<summary>`) é clicável — clicar nele
|
||||
abre/fecha a seção. Uma seta (▸ fechado / ▾ aberto) ao lado do título
|
||||
indica visualmente o estado atual.
|
||||
- Um botão no estilo dos demais botões do painel (`Mostrar` / `Ocultar`)
|
||||
fica ao lado do título, dentro do mesmo `<summary>`. Como ele está dentro
|
||||
do `<summary>`, clicar nele já aciona o toggle nativo do `<details>` —
|
||||
não precisa de lógica de toggle separada. Um script pequeno escuta o
|
||||
evento `toggle` de cada `<details>` e só atualiza o texto do botão
|
||||
(`Mostrar`↔`Ocultar`) e o atributo `aria-expanded`, para acessibilidade.
|
||||
|
||||
**Estado não é persistido:** toda vez que a página `/log` é carregada, as
|
||||
duas seções começam fechadas — não guarda se o usuário tinha deixado aberta
|
||||
antes (sem localStorage, sem cookie, sem parâmetro de URL). Simplicidade,
|
||||
sem necessidade adicional hoje.
|
||||
|
||||
**Sem mudança de comportamento:** os dados carregados por seção (limite de
|
||||
20 em "Ações no painel", botão "Limpar log" restrito a administradores
|
||||
dentro dessa seção, listagem de envios do n8n) continuam exatamente como
|
||||
estão hoje — só o wrapper de exibição (fechado/aberto) muda.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Lembrar o estado aberto/fechado entre recarregamentos de página.
|
||||
- Abrir automaticamente alguma seção por padrão (as duas começam fechadas).
|
||||
- Qualquer mudança nos dados exibidos, na paginação, no limite de 20 ou no
|
||||
botão de limpar log.
|
||||
|
||||
## Arquivos afetados
|
||||
|
||||
- `app/templates/log.html` — envolver cada seção em `<details>`/`<summary>`,
|
||||
adicionar o botão Mostrar/Ocultar e a seta indicadora dentro do `summary`.
|
||||
- `app/static/estilo.css` — estilo do `<details>`/`<summary>` (remoção do
|
||||
marcador padrão do navegador, seta customizada que gira com `[open]`,
|
||||
espaçamento do botão dentro do `summary`).
|
||||
- Um pequeno `<script>` inline (ou incluído no bloco já existente do
|
||||
template) para sincronizar o texto do botão com o evento `toggle`.
|
||||
|
||||
## Testes
|
||||
|
||||
- Teste de router (`tests/test_router_log.py`) verificando que a resposta
|
||||
de `GET /log` contém os elementos `<details>` para as duas seções, sem o
|
||||
atributo `open`, confirmando que ambas carregam fechadas por padrão.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Tabela de Clientes: UI/UX — Colunas, Centralização e Densidade
|
||||
|
||||
## Contexto
|
||||
|
||||
A tabela em `/clientes` usa o estilo genérico de `table`/`th`/`td` do
|
||||
projeto (compartilhado com outras telas como `/funcionarios` e `/log`).
|
||||
Isso deixa a tabela de clientes com padding grande demais para o volume de
|
||||
linhas que ela normalmente exibe, todas as colunas alinhadas à esquerda
|
||||
sem distinção (mesmo colunas curtas como Código e Status), e a coluna CNPJ
|
||||
estreita a ponto do valor quebrar em duas linhas.
|
||||
|
||||
## Objetivo
|
||||
|
||||
Melhorar o layout da tabela de `/clientes` — ajuste de largura de colunas,
|
||||
alinhamento/centralização e densidade das linhas — igual para os dois
|
||||
perfis de usuário (admin e padrão), já que hoje não existe distinção de
|
||||
permissão nessa tela e este trabalho não introduz nenhuma.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Qualquer restrição de ação por perfil (admin vs. padrão) — confirmado
|
||||
explicitamente que não é para mudar quem vê/usa o quê nesta tela.
|
||||
- Mudança em dados exibidos, busca, filtro de status, paginação ou nas
|
||||
ações em si (Abrir, Gerar chave, alternar status).
|
||||
- Mudança no estilo genérico de `table`/`th`/`td` usado por outras telas
|
||||
(`/funcionarios`, `/log`, etc.) — os ajustes ficam restritos a uma classe
|
||||
específica da tabela de clientes, sem tocar nas regras compartilhadas.
|
||||
|
||||
## Design
|
||||
|
||||
**Escopo do CSS:** a tabela de `/clientes` ganha uma classe própria (ex.:
|
||||
`table-clientes`) para que os ajustes de largura/alinhamento/densidade não
|
||||
vazem para as demais tabelas do painel.
|
||||
|
||||
**Larguras e alinhamento por coluna:**
|
||||
| Coluna | Largura | Alinhamento |
|
||||
|---|---|---|
|
||||
| Código | estreita e fixa | centralizado, `tabular-nums` |
|
||||
| Razão | flexível (resto do espaço) | esquerda |
|
||||
| Fantasia | flexível (resto do espaço) | esquerda |
|
||||
| CNPJ | fixa, larga o bastante para não quebrar linha (18 caracteres) | centralizado, `tabular-nums`, `white-space: nowrap` |
|
||||
| Status | estreita e fixa | centralizado |
|
||||
| Ações (Abrir / Gerar chave) | fixa | direita |
|
||||
|
||||
Razão e Fantasia dividem o espaço restante igualmente entre si (cada uma
|
||||
recebendo a mesma largura flexível), já que são as colunas de leitura mais
|
||||
importantes da tabela.
|
||||
|
||||
**Cabeçalhos:** cada `<th>` reflete o alinhamento da coluna correspondente
|
||||
(Código/CNPJ/Status centralizados, Razão/Fantasia à esquerda, Ações à
|
||||
direita) — hoje todos os cabeçalhos ficam forçados à esquerda
|
||||
independente do conteúdo da coluna.
|
||||
|
||||
**Densidade:** o padding vertical das células dessa tabela cai de `10px`
|
||||
para `7px` (mantendo o padding horizontal), permitindo mais linhas visíveis
|
||||
sem rolagem. Os botões de ação (Abrir / Gerar chave) ganham uma classe
|
||||
específica de tabela que remove a margem superior de 14px do botão padrão
|
||||
(pensada para formulários, não para dentro de células) e usa padding mais
|
||||
compacto, mantendo o espaçamento consistente entre os dois botões.
|
||||
|
||||
**Sem mudança de comportamento:** os links de ordenação nos cabeçalhos
|
||||
(`ordenar_por`/`direcao`), a bolinha de status clicável (toggle
|
||||
ativo/inativo), o botão "Gerar chave" e a paginação continuam funcionando
|
||||
exatamente como hoje — só a apresentação visual muda.
|
||||
|
||||
## Testes
|
||||
|
||||
- Teste de router (`tests/test_router_clientes.py`) verificando que a
|
||||
tabela renderizada usa a classe `table-clientes` (confirma que o CSS
|
||||
com escopo está sendo aplicado, sem checar detalhes visuais que testes
|
||||
automatizados não conseguem verificar de forma confiável).
|
||||
Reference in New Issue
Block a user