diff --git a/app/db/clientes.py b/app/db/clientes.py index 039c7a3..0470f0b 100644 --- a/app/db/clientes.py +++ b/app/db/clientes.py @@ -92,17 +92,17 @@ def listar_clientes_para_fiscal() -> list[dict]: o Fiscal espelha TODOS os clientes (ativos e inativos — inativo continua aparecendo em histórico do lado de lá), nunca só uma página. Payload mínimo: ID é a PK técnica estável (CODIGO é editável, não serve de - identidade — mesma razão de FUNCIONARIO_PAINEL.ID no SSO). Sem CNPJ/ - endereço/contato: o Fiscal não precisa desses campos hoje.""" + identidade — mesma razão de FUNCIONARIO_PAINEL.ID no SSO). CNPJ (só dígitos) entra só para a busca/identificação no seletor do + Fiscal; sem endereço/contato.""" conn = get_connection() try: with conn.cursor() as cur: cur.execute( - "SELECT ID, CODIGO, COALESCE(NULLIF(FANTASIA, ''), RAZAO) AS NOME, ATIVO " + "SELECT ID, CODIGO, CNPJ, COALESCE(NULLIF(FANTASIA, ''), RAZAO) AS NOME, ATIVO " "FROM CLIENTE ORDER BY ID" ) return [ - {"external_client_id": row["ID"], "code": row["CODIGO"], "display_name": row["NOME"], "active": bool(row["ATIVO"])} + {"external_client_id": row["ID"], "code": row["CODIGO"], "display_name": row["NOME"], "cnpj": "".join(ch for ch in (row["CNPJ"] or "") if ch.isdigit()) or None, "active": bool(row["ATIVO"])} for row in cur.fetchall() ] finally: diff --git a/docs/GTI_GERENCIADOR_DISCOVERY_V1.md b/docs/GTI_GERENCIADOR_DISCOVERY_V1.md new file mode 100644 index 0000000..6c96b74 --- /dev/null +++ b/docs/GTI_GERENCIADOR_DISCOVERY_V1.md @@ -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 ``. +- **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 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. +``` diff --git a/tests/test_internal_fiscal.py b/tests/test_internal_fiscal.py index b9c2be0..b374bf1 100644 --- a/tests/test_internal_fiscal.py +++ b/tests/test_internal_fiscal.py @@ -50,10 +50,10 @@ def test_returns_active_and_inactive_clients_with_minimal_fields(client, cliente response = client.get("/internal/fiscal/clients", headers={"Authorization": f"Bearer {TOKEN}"}) assert response.status_code == 200 by_id = {row["external_client_id"]: row for row in response.json()["clients"]} - assert by_id[ativo_id] == {"external_client_id": ativo_id, "code": "999950", "display_name": "Ativo Fiscal Teste", "active": True} - assert by_id[inativo_id] == {"external_client_id": inativo_id, "code": "999951", "display_name": "Inativo Fiscal Teste Razao", "active": False} + assert by_id[ativo_id] == {"external_client_id": ativo_id, "code": "999950", "display_name": "Ativo Fiscal Teste", "cnpj": "11222333000181", "active": True} + assert by_id[inativo_id] == {"external_client_id": inativo_id, "code": "999951", "display_name": "Inativo Fiscal Teste Razao", "cnpj": "11222333000181", "active": False} # never leaks fields the Fiscal doesn't need - assert set(by_id[ativo_id]) == {"external_client_id", "code", "display_name", "active"} + assert set(by_id[ativo_id]) == {"external_client_id", "code", "display_name", "cnpj", "active"} def test_missing_token_rejected(client, clientes_teste):