chore: triagem de arquivos untracked (60+ → 0)
Adiciona ao repo os scripts e configs reais do motor que estavam fora do git,
e remove arquivos legados/abandonados.
Adicionados (30):
- scripts/docker-compose.m5.yml, openwa-compose.yml, minio-compose.yml
- scripts/openwa.env (sem secrets)
- scripts/{entrypoint,saleor-entrypoint,saleor-start}.sh
- scripts/{chatwoot-setup,compliance-check,health-check,resumo-diario}.sh
- scripts/{insert_workflow,telegram_user_login,telegram_user_monitor}.py
- scripts/requirements.txt
- scripts/traefik-conf/{traefik.yml,dynamic-config.yml}
- scripts/compliance/, scripts/calculators/, scripts/backups/
- scripts/{README.md,README-INSTALL.md,WHATSAPP-SETUP.md,.gitignore}
- .gitignore (atualizado)
Removidos (ja' em backup/untracked-lixo-2026-07-06/):
- agents/, crm/, hermes_skills_backup/, api-complive/, docs/ (legados)
- scripts/{wrapper.py,wrapper.sh}
- scripts/docker-compose.m5.yml.backup, traefik-conf/routes.yml.bak2
- scripts/saleor-media/RSA_*.{bak,old_*}
- scripts/n8n-workflows/webhook-saleor.json.bak2
- scripts/backups/20260622_0300/saleor_db.sql
- integracao_completa.sh, monitoramento_vps.sh
- ruvector.db (vazio)
Ignorados via .gitignore:
- vault/ (Obsidian KB)
- skills/ (duplicado de ~/.hermes/skills/)
- scripts/{saleor-media,traefik-acme,yt-pub-livesx,minio,n8n-workflows,skills}/
- scripts/backups/2026*_*/
This commit is contained in:
parent
d81347e9fc
commit
9c2f7ff7b4
28
.gitignore
vendored
28
.gitignore
vendored
@ -34,3 +34,31 @@ logs/
|
|||||||
*.bak
|
*.bak
|
||||||
*.bak.*
|
*.bak.*
|
||||||
CLAUDE.md.bak.*
|
CLAUDE.md.bak.*
|
||||||
|
|
||||||
|
# Vault Obsidian (fonte de verdade, replicado fora do git)
|
||||||
|
vault/
|
||||||
|
|
||||||
|
# Skills duplicadas (origem em ~/.hermes/skills/)
|
||||||
|
skills/
|
||||||
|
scripts/skills/
|
||||||
|
|
||||||
|
# Saleor media (chaves RSA + avatares - NUNCA commitar)
|
||||||
|
scripts/saleor-media/
|
||||||
|
|
||||||
|
# Traefik ACME certs (contem email pessoal)
|
||||||
|
scripts/traefik-acme/
|
||||||
|
|
||||||
|
# yt-pub-livesx credenciais criptografadas + dados
|
||||||
|
scripts/yt-pub-livesx/
|
||||||
|
|
||||||
|
# Minio inteiro (data + config local)
|
||||||
|
scripts/minio/
|
||||||
|
|
||||||
|
# n8n workflows locais duplicados (origem em /workflows/)
|
||||||
|
scripts/n8n-workflows/
|
||||||
|
|
||||||
|
# Backups SQL locais antigos
|
||||||
|
scripts/backups/2026*_*/
|
||||||
|
|
||||||
|
# scripts/scripts/ (scripts auxiliares - operacional, nao versionar)
|
||||||
|
/scripts/scripts/
|
||||||
|
|||||||
45
scripts/.gitignore
vendored
Normal file
45
scripts/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Gitignore — M5 E-commerce Scripts
|
||||||
|
# Arquivos que a IA NÃO deve ler (evitam token waste)
|
||||||
|
|
||||||
|
# ── IA / Agentes ──────────────────────────────────────────────────
|
||||||
|
.hermes/
|
||||||
|
.claude-flow/
|
||||||
|
.swarm/
|
||||||
|
node_modules/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# ── Obsidian (não é código) ──────────────────────────────────────
|
||||||
|
# Obsidian vault para projetos seria .obsidian/
|
||||||
|
.vault_backup/
|
||||||
|
|
||||||
|
# ── Build / Dist ────────────────────────────────────────────────
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
|
||||||
|
# ── Docker ───────────────────────────────────────────────────────
|
||||||
|
*.tar.gz # backups gerados automaticamene
|
||||||
|
|
||||||
|
# ── Credenciais ─────────────────────────────────────────────────
|
||||||
|
credentials.json
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
secrets/
|
||||||
|
vault/
|
||||||
|
|
||||||
|
# ── Large files ─────────────────────────────────────────────────
|
||||||
|
*.mp4
|
||||||
|
*.mov
|
||||||
|
*.zip > 5MB
|
||||||
|
*.iso
|
||||||
220
scripts/README-INSTALL.md
Normal file
220
scripts/README-INSTALL.md
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
# README-INSTALL.md — Stack M5 Brasil
|
||||||
|
**Motor Ecommerce M5 | Setup Autônomo com Docker Compose**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
| Ferramenta | Versão mínima | Instalação |
|
||||||
|
|---|---|---|
|
||||||
|
| Docker Desktop | 4.x+ | https://docs.docker.com/desktop/install/windows-install/ |
|
||||||
|
| Git Bash / MSYS | (já incluso no Git) | https://git-scm.com/download/win |
|
||||||
|
| curl | (já incluso) | — |
|
||||||
|
|
||||||
|
Verifique a instalação:
|
||||||
|
```bash
|
||||||
|
docker --version # Docker version 2x.x+
|
||||||
|
docker compose version # Docker Compose version v2.x+
|
||||||
|
git --version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Instalação — Passo a Passo
|
||||||
|
|
||||||
|
### 1. Clonar / Copiar o projeto
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Projects
|
||||||
|
git clone <seu-repo-m5> ecommerce-scripts
|
||||||
|
cd ecommerce-scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurar variáveis de ambiente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copiar o template
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Editar com seus dados reais
|
||||||
|
nano .env # ou code .env, notepad, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Campos obrigatórios a preencher em `.env`:**
|
||||||
|
```
|
||||||
|
BASE_DOMAIN=seudominio.com.br
|
||||||
|
ACME_EMAIL=seu@email.com.br
|
||||||
|
POSTGRES_PASSWORD=<senha forte>
|
||||||
|
REDIS_PASSWORD=<senha forte>
|
||||||
|
SALEOR_SECRET_KEY=<chave aleatória>
|
||||||
|
N8N_PASSWORD=<senha>
|
||||||
|
STRIPE_API_KEY=<chave real>
|
||||||
|
EMAIL_URL=smtp://usuario:senha@smtp.seudominio.com.br:587
|
||||||
|
```
|
||||||
|
|
||||||
|
> Gere uma SECRET_KEY segura: `openssl rand -hex 32`
|
||||||
|
|
||||||
|
### 3. Configurar DNS para os subdomínios
|
||||||
|
|
||||||
|
No painel DNS do seu domínio (`seudominio.com.br`), crie os registros **A** ou **CNAME**:
|
||||||
|
|
||||||
|
| Subdomínio | Tipo | Valor |
|
||||||
|
|---|---|---|
|
||||||
|
| `vendas` | A | IP do servidor |
|
||||||
|
| `n8n` | A | IP do servidor |
|
||||||
|
| `git` | A | IP do servidor |
|
||||||
|
| `wa` | A | IP do servidor |
|
||||||
|
|
||||||
|
Se usar Cloudflare, configure o proxy accordingly.
|
||||||
|
|
||||||
|
### 4. Iniciar a stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tornar scripts executáveis (Linux/WSL/Git Bash)
|
||||||
|
chmod +x SCRIPTS/*.sh
|
||||||
|
|
||||||
|
# Iniciar todos os serviços
|
||||||
|
./SCRIPTS/start-m5.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O script faz:
|
||||||
|
1. Pull das imagens Docker
|
||||||
|
2. `docker compose up -d`
|
||||||
|
3. Healthcheck de cada serviço (aguarda até 90s por serviço)
|
||||||
|
4. Exibe URLs de acesso
|
||||||
|
|
||||||
|
### 5. Setup pós-instalação
|
||||||
|
|
||||||
|
#### Saleor — primeiro acesso
|
||||||
|
```bash
|
||||||
|
# Criar superusuário (executar após Saleor estar pronto)
|
||||||
|
docker exec -it m5_saleor python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Acesse `https://vendas.seudominio.com.br/dashboard`
|
||||||
|
2. Configure canal de vendas (Mercado Brasil)
|
||||||
|
3. Cadastre meio de pagamento (Stripe / Mercado Pago)
|
||||||
|
|
||||||
|
#### n8n — primeiro acesso
|
||||||
|
1. Acesse `https://n8n.seudominio.com.br`
|
||||||
|
2. Login com `N8N_USER` / `N8N_PASSWORD` do `.env`
|
||||||
|
3. Importe workflows de `./n8n-workflows/` (se existirem)
|
||||||
|
|
||||||
|
#### Gitea — primeiro acesso
|
||||||
|
1. Acesse `https://git.seudominio.com.br`
|
||||||
|
2. Complete o wizard de instalação (banco já configurado via env)
|
||||||
|
3. Crie usuário admin
|
||||||
|
|
||||||
|
#### OpenWA — primeira sessão
|
||||||
|
1. Acesse `http://localhost:8081` (dashboard local)
|
||||||
|
2. Escaneie QR Code com WhatsApp desejado
|
||||||
|
3. Configure webhook para `http://n8n:5678/webhook/wa`
|
||||||
|
|
||||||
|
### 6. Cron — backup automático
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Editar crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# Adicionar linha (backup diário às 3h da manhã):
|
||||||
|
0 3 * * * /c/Users/Iris/Projects/ecommerce-scripts/SCRIPTS/backup-vault.sh >> ~/backups/vault/cron.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Serviços — Visão Geral
|
||||||
|
|
||||||
|
| Serviço | Imagem | Porta local | URL |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Postgres | postgres:16-alpine | 5432 | (interno) |
|
||||||
|
| Redis | redis:7-alpine | 6379 | (interno) |
|
||||||
|
| Saleor (API) | ghcr.io/saleor-commerce/saleor | 8000 | `vendas.*` |
|
||||||
|
| Saleor Dashboard | ghcr.io/saleor-commerce/saleor-dashboard | 80 | `vendas.*/dashboard` |
|
||||||
|
| n8n | n8nio/n8n | 5678 | `n8n.*` |
|
||||||
|
| OpenWA | openwa/wa-automate | 8080/8081 | `wa.*` |
|
||||||
|
| Gitea | gitea/gitea | 3000 | `git.*` |
|
||||||
|
| Traefik | traefik:v3.1 | 80/443/8090 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operações do Dia a Dia
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ver status
|
||||||
|
./SCRIPTS/status-m5.sh
|
||||||
|
|
||||||
|
# Ver logs de um serviço específico
|
||||||
|
docker compose -f docker-compose.m5.yml logs -f saleor
|
||||||
|
docker compose -f docker-compose.m5.yml logs -f n8n
|
||||||
|
|
||||||
|
# Parar stack (preserva dados)
|
||||||
|
./SCRIPTS/stop-m5.sh
|
||||||
|
|
||||||
|
# Reiniciar apenas um serviço
|
||||||
|
docker compose -f docker-compose.m5.yml restart saleor
|
||||||
|
|
||||||
|
# Rebuild de um serviço (após atualização)
|
||||||
|
docker compose -f docker-compose.m5.yml up -d --force-recreate saleor
|
||||||
|
|
||||||
|
# Limpar tudo (PERDE DADOS)
|
||||||
|
./SCRIPTS/stop-m5.sh --volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integrações
|
||||||
|
|
||||||
|
### Mercado Livre → Saleor (via n8n)
|
||||||
|
- Workflow n8n: `webhook → ML Order Created → criar pedido no Saleor`
|
||||||
|
- Autenticação: OAuth App do Mercado Livre
|
||||||
|
- Webhook ML: apontar para `https://n8n.seudominio.com.br/webhook/ml-pedido`
|
||||||
|
|
||||||
|
### WhatsApp → n8n → Saleor
|
||||||
|
- OpenWA conecta via API REST na porta 8080
|
||||||
|
- n8n recebe webhooks do OpenWA
|
||||||
|
- Workflow: `msg recebida → verificar produto → enviar catálogo → criar pedido`
|
||||||
|
|
||||||
|
### Stripe (pagamentos)
|
||||||
|
- Webhook: `https://seudominio.com.br/webhook/stripe`
|
||||||
|
- Saleor processa pagamentos via plugin Stripe oficial
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Segurança
|
||||||
|
|
||||||
|
- **Nunca comite `.env`** — já está em `.gitignore`
|
||||||
|
- **Renove senhas periodicamente** (sempre rebuild após mudança de `.env`)
|
||||||
|
- **Firewall**: exponha apenas 80, 443 e 8090 (Traefik dashboard)
|
||||||
|
- **OpenWA**: se possível, limite acesso ao IP do servidor para a API local
|
||||||
|
- **Traefik Dashboard**: proteja com Basic Auth ou IP whitelist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Problema | Solução |
|
||||||
|
|---|---|
|
||||||
|
| `postgres permission denied` | `docker compose down -v` (apaga volumes) e `start-m5.sh` novamente |
|
||||||
|
| SSL não funciona | Verificar registros DNS e se porta 443 está aberta |
|
||||||
|
| n8n não conecta no Saleor | `SALEOR_API_URL` deve ser `http://saleor:8000/graphql/` (interno) |
|
||||||
|
| OpenWA QR Code não aparece | `docker logs m5_openwa` para ver erros de sessão |
|
||||||
|
| Traefik sem certificados | `docker logs m5_traefik` — verificar email ACME |
|
||||||
|
| Saleor 502 | Aguardar ~2min após start; checar `docker logs m5_saleor` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Atualização de Serviços
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Atualizar imagem e rebuildar
|
||||||
|
docker compose -f docker-compose.m5.yml pull saleor
|
||||||
|
docker compose -f docker-compose.m5.yml up -d --force-recreate saleor
|
||||||
|
|
||||||
|
# Atualizar todos de uma vez
|
||||||
|
docker compose -f docker-compose.m5.yml pull
|
||||||
|
docker compose -f docker-compose.m5.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Criado para o Motor Ecommerce M5 Brasil — Iris Florencio — 2026*
|
||||||
38
scripts/README.md
Normal file
38
scripts/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# E-commerce Scripts
|
||||||
|
|
||||||
|
Scripts Python para operações de e-commerce multi-canal.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Projects/ecommerce-scripts
|
||||||
|
source venv/Scripts/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts Disponíveis
|
||||||
|
|
||||||
|
### Calculadoras
|
||||||
|
- `calculators/marketplace_margin.py` — Calcula margem líquida por marketplace
|
||||||
|
- `calculators/freight_quote.py` — Cotação de frete por CEP e peso
|
||||||
|
- `calculators/cac_ltv.py` — Calcula CAC, LTV e ROAS
|
||||||
|
|
||||||
|
### Marketplaces
|
||||||
|
- `marketplaces/shopee_bulk.py` — Upload em massa para Shopee
|
||||||
|
- `marketplaces/ml_pricing.py` — Calculadora de preço para Mercado Livre
|
||||||
|
|
||||||
|
### Automação
|
||||||
|
- `automation/price_monitor.py` — Monitoramento de preços de concorrentes
|
||||||
|
- `automation/order_tracker.py` — Rastreamento de pedidos
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
|
||||||
|
```
|
||||||
|
ecommerce-scripts/
|
||||||
|
├── calculators/
|
||||||
|
├── marketplaces/
|
||||||
|
├── automation/
|
||||||
|
├── data/
|
||||||
|
├── requirements.txt
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
30
scripts/WHATSAPP-SETUP.md
Normal file
30
scripts/WHATSAPP-SETUP.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# WhatsApp Setup — OpenWA (Motor M5)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Container `m5_openwa` está rodando e aguardando autenticação.
|
||||||
|
|
||||||
|
## Acesso ao QR Code
|
||||||
|
1. Abrir navegador: **http://31.220.78.126:8002/**
|
||||||
|
2. Escanear QR code com o WhatsApp do número **+55 11 970168239**
|
||||||
|
3. Após escanear, a sessão será salva automaticamente
|
||||||
|
|
||||||
|
## Alternativa: via terminal (VPS)
|
||||||
|
```bash
|
||||||
|
docker logs -f m5_openwa
|
||||||
|
# O QR code aparece nos logs quando o container inicia
|
||||||
|
```
|
||||||
|
|
||||||
|
## Após autenticação
|
||||||
|
- Sessão salva em `/root/ecommerce/scripts/openwa-sessions/`
|
||||||
|
- Webhook: `http://n8n:5678/webhook/wa` (já configurado no compose)
|
||||||
|
- Testar com n8n: criar workflow que recebe mensagens do WhatsApp
|
||||||
|
|
||||||
|
## Número vinculado
|
||||||
|
- **+55 11 970168239** (número do cliente/fornecedor a definir)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
Se o QR expirar:
|
||||||
|
```bash
|
||||||
|
docker restart m5_openwa
|
||||||
|
# Aguardar 15s e acessar http://31.220.78.126:8002/
|
||||||
|
```
|
||||||
34
scripts/backups/backup-vault.bat
Normal file
34
scripts/backups/backup-vault.bat
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
@echo off
|
||||||
|
REM ============================================================
|
||||||
|
REM backup-vault.bat — Wrapper Windows para backup do Vault
|
||||||
|
REM Chama o script bash (Git Bash / MSYS2)
|
||||||
|
REM Agendar via Task Scheduler: schtasks /create ...
|
||||||
|
REM ============================================================
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
SET SCRIPT_DIR=%~dp0
|
||||||
|
SET BASH_PATH=C:\Program Files\Git\bin\bash.exe
|
||||||
|
|
||||||
|
IF NOT EXIST "%BASH_PATH%" (
|
||||||
|
SET BASH_PATH=C:\Program Files (x86)\Git\bin\bash.exe
|
||||||
|
)
|
||||||
|
|
||||||
|
IF NOT EXIST "%BASH_PATH%" (
|
||||||
|
echo [ERRO] Git Bash nao encontrado. Instale Git for Windows.
|
||||||
|
echo Download: https://git-scm.com/download/win
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [INFO] Executando backup via Git Bash...
|
||||||
|
"%BASH_PATH%" --login -i "%SCRIPT_DIR%backup-vault.sh"
|
||||||
|
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo [ERRO] Backup falhou com codigo: %ERRORLEVEL%
|
||||||
|
pause
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [OK] Backup concluido.
|
||||||
|
pause
|
||||||
5
scripts/backups/backup-vault.sh
Executable file
5
scripts/backups/backup-vault.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# M5 - Backup Vault Diário
|
||||||
|
# Data: 24/06/2026
|
||||||
|
echo "Backup Vault - $(date)"
|
||||||
|
# Será implementado com rsync/backup
|
||||||
27
scripts/backups/cron-backup.sh
Executable file
27
scripts/backups/cron-backup.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# cron-backup.sh — Wrapper para cron job Hermes
|
||||||
|
# Chamado automaticamente todo dia às 02:00
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
LOG_FILE="$SCRIPT_DIR/../logs/cron-backup.log"
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
|
exec >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Cron backup iniciado: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
bash "$SCRIPT_DIR/backup-vault.sh"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
|
echo "Backup concluído com sucesso: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
else
|
||||||
|
echo "ERRO no backup - código: $EXIT_CODE - $(date '+%Y-%m-%d %H:%M:%S')" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
191
scripts/backups/restore-vault.sh
Executable file
191
scripts/backups/restore-vault.sh
Executable file
@ -0,0 +1,191 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# restore-vault.sh — Restaurar backup do Vault Obsidian
|
||||||
|
# Uso: ./restore-vault.sh [YYYYMMDD_HHMMSS]
|
||||||
|
# ./restore-vault.sh list → lista backups disponíveis
|
||||||
|
# ./restore-vault.sh latest → restaura o mais recente
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VAULT_DIR="C:/Users/Iris/Vaults/ecommerce-kb"
|
||||||
|
BACKUP_DIR="C:/Users/Iris/backups/vault"
|
||||||
|
LOG_FILE="$BACKUP_DIR/restore.log"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mostrar ajuda
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
${GREEN}restore-vault.sh — Restaurar Vault Obsidian${NC}
|
||||||
|
|
||||||
|
${YELLOW}Uso:${NC}
|
||||||
|
$0 list Lista backups disponíveis
|
||||||
|
$0 latest Restaura o backup mais recente
|
||||||
|
$0 <timestamp> Restaura backup específico (ex: 20250617_020000)
|
||||||
|
$0 dry-run <timestamp> Simula restauração (sem sobrescrever)
|
||||||
|
|
||||||
|
${YELLOW}Exemplos:${NC}
|
||||||
|
$0 list
|
||||||
|
$0 latest
|
||||||
|
$0 20250617_020000
|
||||||
|
|
||||||
|
${YELLOW}Arquivos de backup:${NC}
|
||||||
|
$BACKUP_DIR/vault_YYYYMMDD_HHMMSS.tar.gz
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Listar backups disponíveis
|
||||||
|
list_backups() {
|
||||||
|
log "${BLUE}=== BACKUPS DISPONÍVEIS ===${NC}"
|
||||||
|
echo ""
|
||||||
|
if ls "$BACKUP_DIR"/vault_*.tar.gz 1>/dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}ID | Timestamp | Tamanho | Data${NC}"
|
||||||
|
echo -e "${GREEN}----|---------------------|---------|----------------${NC}"
|
||||||
|
ID=1
|
||||||
|
for file in $(ls -t "$BACKUP_DIR"/vault_*.tar.gz); do
|
||||||
|
TIMESTAMP=$(basename "$file" | sed 's/vault_\(.*\)\.tar\.gz/\1/')
|
||||||
|
SIZE=$(du -h "$file" | cut -f1)
|
||||||
|
DATE=$(date -r "$file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
||||||
|
printf "${GREEN}%3d | %s | %6s | %s${NC}\n" "$ID" "$TIMESTAMP" "$SIZE" "$DATE"
|
||||||
|
ID=$((ID + 1))
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
log "Total: $(ls "$BACKUP_DIR"/vault_*.tar.gz | wc -l) backup(s)"
|
||||||
|
else
|
||||||
|
log "${RED}Nenhum backup encontrado em $BACKUP_DIR${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restaurar backup específico
|
||||||
|
restore_backup() {
|
||||||
|
local TIMESTAMP="$1"
|
||||||
|
local DRY_RUN="${2:-false}"
|
||||||
|
|
||||||
|
# Encontrar arquivo de backup
|
||||||
|
if [ -n "$TIMESTAMP" ]; then
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/vault_${TIMESTAMP}.tar.gz"
|
||||||
|
else
|
||||||
|
# Pegar o mais recente
|
||||||
|
BACKUP_FILE=$(ls -t "$BACKUP_DIR"/vault_*.tar.gz 2>/dev/null | head -1)
|
||||||
|
TIMESTAMP=$(basename "$BACKUP_FILE" | sed 's/vault_\(.*\)\.tar\.gz/\1/')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BACKUP_FILE" ]; then
|
||||||
|
log "${RED}ERRO: Backup não encontrado: $(basename "$BACKUP_FILE")${NC}"
|
||||||
|
log "Use '$0 list' para ver backups disponíveis"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${YELLOW}=== INICIANDO RESTAURAÇÃO ===${NC}"
|
||||||
|
log "Backup: $(basename "$BACKUP_FILE")"
|
||||||
|
log "Tamanho: $(du -h "$BACKUP_FILE" | cut -f1)"
|
||||||
|
log "MD5: $(md5sum "$BACKUP_FILE" | cut -d' ' -f1)"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
log "${YELLOW}[DRY-RUN] Simulando extração...${NC}"
|
||||||
|
tar -tzf "$BACKUP_FILE" | head -20
|
||||||
|
log "${YELLOW}[DRY-RUN] Total de arquivos: $(tar -tzf "$BACKUP_FILE" | wc -l)${NC}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Perguntar confirmação
|
||||||
|
echo ""
|
||||||
|
read -p "Isso irá substituir o vault atual em $VAULT_DIR. Continuar? (s/N): " CONFIRM
|
||||||
|
if [ "$CONFIRM" != "s" ] && [ "$CONFIRM" != "S" ]; then
|
||||||
|
log "Resturação cancelada pelo usuário"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Criar backup do estado atual antes de restaurar
|
||||||
|
if [ -d "$VAULT_DIR" ]; then
|
||||||
|
PRE_RESTORE="$BACKUP_DIR/pre_restore_${DATE}_$(date +%Y%m%d_%H%M%S).tar.gz"
|
||||||
|
log "Criando backup do estado atual..."
|
||||||
|
tar -czf "$PRE_RESTORE" -C "$(dirname "$VAULT_DIR")" "$(basename "$VAULT_DIR")" 2>/dev/null || true
|
||||||
|
log "Backup de segurança: $(basename "$PRE_RESTORE")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extrair backup
|
||||||
|
log "Extraindo backup..."
|
||||||
|
|
||||||
|
# Backup do vault atual (se existir)
|
||||||
|
if [ -d "$VAULT_DIR" ]; then
|
||||||
|
TEMP_VAULT="${VAULT_DIR}_temp_$$"
|
||||||
|
mv "$VAULT_DIR" "$TEMP_VAULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extrair novo backup
|
||||||
|
mkdir -p "$VAULT_DIR"
|
||||||
|
if tar -xzf "$BACKUP_FILE" -C "$(dirname "$VAULT_DIR")"; then
|
||||||
|
# O tar extrai para ecommerce-kb/ dentro do diretório pai
|
||||||
|
EXTRACTED_DIR="$(dirname "$VAULT_DIR")/$(basename "$VAULT_DIR")"
|
||||||
|
if [ -d "$EXTRACTED_DIR" ] && [ "$EXTRACTED_DIR" != "$VAULT_DIR" ]; then
|
||||||
|
# Mover conteúdo extraído para o local correto
|
||||||
|
mv "$EXTRACTED_DIR"/* "$VAULT_DIR/" 2>/dev/null || true
|
||||||
|
rm -rf "$EXTRACTED_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Limpar vault temporário
|
||||||
|
if [ -n "$TEMP_VAULT" ]; then
|
||||||
|
rm -rf "$TEMP_VAULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}=== RESTAURAÇÃO CONCLUÍDA COM SUCESSO ===${NC}"
|
||||||
|
log "Vault restaurado: $VAULT_DIR"
|
||||||
|
log "Arquivos: $(find "$VAULT_DIR" -type f 2>/dev/null | wc -l)"
|
||||||
|
log "Backup usado: $(basename "$BACKUP_FILE")"
|
||||||
|
|
||||||
|
# Atualizar status
|
||||||
|
if [ -f "$BACKUP_DIR/latest_status.json" ]; then
|
||||||
|
log "Arquivo de status atualizado."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Restaurar estado anterior em caso de erro
|
||||||
|
if [ -n "$TEMP_VAULT" ]; then
|
||||||
|
mv "$TEMP_VAULT" "$VAULT_DIR"
|
||||||
|
fi
|
||||||
|
log "${RED}ERRO: Falha ao extrair backup${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== MAIN ====================
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
list|l|ls)
|
||||||
|
list_backups
|
||||||
|
;;
|
||||||
|
latest)
|
||||||
|
restore_backup ""
|
||||||
|
;;
|
||||||
|
dry-run)
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
log "${RED}ERRO: Especifique o timestamp para dry-run${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
restore_backup "$2" "true"
|
||||||
|
;;
|
||||||
|
help|-h|--help)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
if [ -f "$BACKUP_DIR/latest_status.json" ]; then
|
||||||
|
cat "$BACKUP_DIR/latest_status.json"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
restore_backup "$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
187
scripts/calculators/marketplace_margin.py
Executable file
187
scripts/calculators/marketplace_margin.py
Executable file
@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Calculadora de Margem Líquida por Marketplace
|
||||||
|
Calcula o lucro real por canal considerando todas as fees.
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python calculators/marketplace_margin.py
|
||||||
|
python calculators/marketplace_margin.py --price 120 --cost 40 --marketplace shopee
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.prompt import FloatPrompt
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
MARKETPLACES = {
|
||||||
|
"shopee": {
|
||||||
|
"fee": 0.18,
|
||||||
|
"fee_name": "Taxa de Venda Shopee",
|
||||||
|
"payment_fee": 0.029,
|
||||||
|
"payment_name": "Taxa de Pagamento",
|
||||||
|
"shipping_subsidy": 0.10,
|
||||||
|
"shipping_name": "Subsídio Frete (média)",
|
||||||
|
"pkg_fee": 0.03,
|
||||||
|
"pkg_name": "Embalagem (média)",
|
||||||
|
},
|
||||||
|
"mercadolivre": {
|
||||||
|
"fee": 0.16,
|
||||||
|
"fee_name": "Taxa de Venda ML",
|
||||||
|
"payment_fee": 0.039,
|
||||||
|
"payment_name": "Taxa de Pagamento ML",
|
||||||
|
"shipping_subsidy": 0.10,
|
||||||
|
"shipping_name": "Subsídio Frete (média)",
|
||||||
|
"pkg_fee": 0.03,
|
||||||
|
"pkg_name": "Embalagem (média)",
|
||||||
|
},
|
||||||
|
"amazon": {
|
||||||
|
"fee": 0.15,
|
||||||
|
"fee_name": "Referral Fee Amazon",
|
||||||
|
"payment_fee": 0.0,
|
||||||
|
"payment_name": "Taxa de Pagamento (incluída)",
|
||||||
|
"shipping_subsidy": 0.08,
|
||||||
|
"shipping_name": "Custo Envio FBA (média)",
|
||||||
|
"pkg_fee": 0.04,
|
||||||
|
"pkg_name": "FBA Pick & Pack (média)",
|
||||||
|
},
|
||||||
|
"tiktok": {
|
||||||
|
"fee": 0.08,
|
||||||
|
"fee_name": "Taxa de Venda TikTok Shop",
|
||||||
|
"payment_fee": 0.019,
|
||||||
|
"payment_name": "Taxa de Pagamento",
|
||||||
|
"shipping_subsidy": 0.08,
|
||||||
|
"shipping_name": "Frete TikTok (média)",
|
||||||
|
"pkg_fee": 0.02,
|
||||||
|
"pkg_name": "Embalagem (média)",
|
||||||
|
},
|
||||||
|
"magalu": {
|
||||||
|
"fee": 0.19,
|
||||||
|
"fee_name": "Taxa de Venda Magalu",
|
||||||
|
"payment_fee": 0.025,
|
||||||
|
"payment_name": "Taxa de Pagamento",
|
||||||
|
"shipping_subsidy": 0.11,
|
||||||
|
"shipping_name": "Subsídio Frete (média)",
|
||||||
|
"pkg_fee": 0.03,
|
||||||
|
"pkg_name": "Embalagem (média)",
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"fee": 0.0,
|
||||||
|
"fee_name": "Fee Plataforma (Shopify ~2%)",
|
||||||
|
"payment_fee": 0.029,
|
||||||
|
"payment_name": "Taxa de Pagamento (Stripe/PagSeguro)",
|
||||||
|
"shipping_subsidy": 0.08,
|
||||||
|
"shipping_name": "Frete médio (Correios/próprio)",
|
||||||
|
"pkg_fee": 0.03,
|
||||||
|
"pkg_name": "Embalagem",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_margin(price: float, cost: float, marketplace: dict, channel_name: str):
|
||||||
|
"""Calcula margem para um canal."""
|
||||||
|
gross = price - cost
|
||||||
|
|
||||||
|
fee_amount = price * marketplace["fee"]
|
||||||
|
payment_amount = price * marketplace["payment_fee"]
|
||||||
|
shipping_amount = price * marketplace["shipping_subsidy"]
|
||||||
|
pkg_amount = price * marketplace["pkg_fee"]
|
||||||
|
|
||||||
|
total_fees = fee_amount + payment_amount + shipping_amount + pkg_amount
|
||||||
|
net_profit = gross - total_fees
|
||||||
|
margin_pct = (net_profit / price * 100) if price > 0 else 0
|
||||||
|
roi = ((net_profit / cost) * 100) if cost > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"canal": channel_name,
|
||||||
|
"preco": price,
|
||||||
|
"custo": cost,
|
||||||
|
"lucro_bruto": gross,
|
||||||
|
"fee_venda": fee_amount,
|
||||||
|
"fee_pagto": payment_amount,
|
||||||
|
"fee_frete": shipping_amount,
|
||||||
|
"fee_pkg": pkg_amount,
|
||||||
|
"total_fees": total_fees,
|
||||||
|
"lucro_liquido": net_profit,
|
||||||
|
"margem_pct": margin_pct,
|
||||||
|
"roi_pct": roi,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_result(r: dict):
|
||||||
|
table = Table(title=f"📊 Margem — {r['canal']}", style="cyan")
|
||||||
|
table.add_column("Item", style="yellow")
|
||||||
|
table.add_column("Valor", justify="right", style="white")
|
||||||
|
|
||||||
|
table.add_row("Preço de Venda", f"R$ {r['preco']:.2f}")
|
||||||
|
table.add_row("Custo do Produto", f"R$ {r['custo']:.2f}")
|
||||||
|
table.add_row("Lucro Bruto", f"R$ {r['lucro_bruto']:.2f}")
|
||||||
|
table.add_row(" Fee de Venda", f"− R$ {r['fee_venda']:.2f}")
|
||||||
|
table.add_row(" Fee de Pagamento", f"− R$ {r['fee_pagto']:.2f}")
|
||||||
|
table.add_row(" Subsídio Frete", f"− R$ {r['fee_frete']:.2f}")
|
||||||
|
table.add_row(" Embalagem", f"− R$ {r['fee_pkg']:.2f}")
|
||||||
|
table.add_row("TOTAL DE FEES", f"− R$ {r['total_fees']:.2f}", style="red")
|
||||||
|
table.add_row("LUCRO LÍQUIDO", f"R$ {r['lucro_liquido']:.2f}", style="green bold")
|
||||||
|
table.add_row("Margem Líquida", f"{r['margem_pct']:.1f}%", style="green")
|
||||||
|
table.add_row("ROI sobre Custo", f"{r['roi_pct']:.1f}%", style="cyan")
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
def print_comparison(price: float, cost: float):
|
||||||
|
"""Compara todos os marketplaces."""
|
||||||
|
table = Table(title="📊 Comparativo de Margem — Preço: R$ {:.2f}".format(price), style="cyan")
|
||||||
|
table.add_column("Canal", style="yellow")
|
||||||
|
table.add_column("Lucro Líquido", justify="right", style="white")
|
||||||
|
table.add_column("Margem %", justify="right")
|
||||||
|
table.add_column("ROI %", justify="right")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for name, config in MARKETPLACES.items():
|
||||||
|
r = calculate_margin(price, cost, config, name.title())
|
||||||
|
results.append(r)
|
||||||
|
|
||||||
|
# Sort by profit
|
||||||
|
results.sort(key=lambda x: x["lucro_liquido"], reverse=True)
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
style = "green" if r["lucro_liquido"] > 0 else "red"
|
||||||
|
table.add_row(
|
||||||
|
r["canal"],
|
||||||
|
f"R$ {r['lucro_liquido']:.2f}",
|
||||||
|
f"{r['margem_pct']:.1f}%",
|
||||||
|
f"{r['roi_pct']:.1f}%",
|
||||||
|
style=style,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
console.print()
|
||||||
|
console.print(f"[yellow]Custo do produto:[/yellow] R$ {cost:.2f} | [yellow]Margem bruta:[/yellow] {((price-cost)/price*100):.1f}%")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Calculadora de margem por marketplace")
|
||||||
|
parser.add_argument("--price", type=float, help="Preço de venda")
|
||||||
|
parser.add_argument("--cost", type=float, help="Custo do produto")
|
||||||
|
parser.add_argument("--marketplace", type=str, choices=list(MARKETPLACES.keys()) + ["all"], default="all")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.price and args.cost:
|
||||||
|
price = args.price
|
||||||
|
cost = args.cost
|
||||||
|
else:
|
||||||
|
console.print("[bold cyan]Calculadora de Margem por Marketplace[/bold cyan]\n")
|
||||||
|
price = FloatPrompt.ask("Preço de venda (R$)", default="120.0")
|
||||||
|
cost = FloatPrompt.ask("Custo do produto (R$)", default="40.0")
|
||||||
|
|
||||||
|
if args.marketplace == "all":
|
||||||
|
print_comparison(price, cost)
|
||||||
|
else:
|
||||||
|
config = MARKETPLACES[args.marketplace]
|
||||||
|
r = calculate_margin(price, cost, config, args.marketplace.title())
|
||||||
|
print_result(r)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
59
scripts/chatwoot-setup.sh
Executable file
59
scripts/chatwoot-setup.sh
Executable file
@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Chatwoot Setup Script
|
||||||
|
# Configurar integração Chatwoot + n8n
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CHATWOOT_URL="https://chat.m5digital.net.br"
|
||||||
|
TOKEN="AwnRiNkenTfzmipP8sM4dewQ"
|
||||||
|
ACCOUNT_ID=1
|
||||||
|
|
||||||
|
echo "🔧 Chatwoot Setup"
|
||||||
|
echo "=================="
|
||||||
|
|
||||||
|
# 1. Verificar status
|
||||||
|
echo -e "\n📊 Status da API:"
|
||||||
|
curl -s "${CHATWOOT_URL}/api" | jq '.'
|
||||||
|
|
||||||
|
# 2. Listar agentes
|
||||||
|
echo -e "\n👥 Agentes:"
|
||||||
|
curl -s -H "api_access_token: ${TOKEN}" \
|
||||||
|
"${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/agents" | jq -r '.data[] | " - \(.name) (\(.role))"'
|
||||||
|
|
||||||
|
# 3. Listar inboxes
|
||||||
|
echo -e "\n📥 Inboxes:"
|
||||||
|
INBOXES=$(curl -s -H "api_access_token: ${TOKEN}" \
|
||||||
|
"${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/inboxes")
|
||||||
|
echo "$INBOXES" | jq -r '.data[] | " - \(.name): \(.channel_type)"' || echo " Nenhum inbox"
|
||||||
|
|
||||||
|
# 4. Criar Inbox Web (API)
|
||||||
|
echo -e "\n🔧 Criando Inbox Web..."
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "api_access_token: ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/inboxes" \
|
||||||
|
-d '{
|
||||||
|
"name": "Website",
|
||||||
|
"channel": {
|
||||||
|
"type": "web",
|
||||||
|
"website_url": "https://loja.m5digital.net.br"
|
||||||
|
}
|
||||||
|
}' | jq -r '.payload.name // .error // "Erro"'
|
||||||
|
|
||||||
|
# 5. Listar Templates de Mensagens
|
||||||
|
echo -e "\n📝 Templates de Mensagens:"
|
||||||
|
curl -s -H "api_access_token: ${TOKEN}" \
|
||||||
|
"${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/message_templates" | jq -r '.data[] | " - \(.name)"' || echo " Nenhum template"
|
||||||
|
|
||||||
|
# 6. Listar Canned Responses
|
||||||
|
echo -e "\n⚡ Respostas Prontas:"
|
||||||
|
curl -s -H "api_access_token: ${TOKEN}" \
|
||||||
|
"${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/canned_responses" | jq -r '.data[] | " - \(.short_code): \(.content[0:50])..."' || echo " Nenhuma resposta pronta"
|
||||||
|
|
||||||
|
echo -e "\n✅ Setup completo!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Próximos passos:"
|
||||||
|
echo "1. Acesse ${CHATWOOT_URL}/app"
|
||||||
|
echo "2. Configure o widget de chat no website"
|
||||||
|
echo "3. Configure o webhook em n8n: ${CHATWOOT_URL}/api/v1/webhooks"
|
||||||
|
echo "4. Configure OpenWA como inbox adicional"
|
||||||
5
scripts/compliance-check.sh
Executable file
5
scripts/compliance-check.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# M5 - Compliance Check
|
||||||
|
# Data: 24/06/2026
|
||||||
|
echo "Compliance Check - $(date)"
|
||||||
|
# Será implementado com validações
|
||||||
406
scripts/compliance/compliance-checker.py
Executable file
406
scripts/compliance/compliance-checker.py
Executable file
@ -0,0 +1,406 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
compliance-checker.py — Motor Ecommerce M5 Brasil
|
||||||
|
Detecta palavras proibidas e verifica conformidade regulatória.
|
||||||
|
Usage: python3 compliance-checker.py --title "..." --description "..."
|
||||||
|
python3 compliance-checker.py --interactive
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 1. LISTAS DE PALAVRAS PROIBIDAS
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
PROHIBITED_ML = {
|
||||||
|
"replica", "clone de", "versão falsa", "cópia 1:1", "original fake",
|
||||||
|
"fake", "falsificado", "pirata", "genérico", "igual ao original",
|
||||||
|
"similar a", "semelhante a",
|
||||||
|
"sem nota", "notaomitida", "semnf", "não emito nota", "sem nota fiscal",
|
||||||
|
"nf free", "nota livre", "não emito", "isento de nota",
|
||||||
|
"remédio", "cura", "trata covid", "combate vírus", "cura naturalmente",
|
||||||
|
"tarja preta", "tarja vermelha", "antibiótico", "anti-inflamatório",
|
||||||
|
"analgésico", "receita médica", "com receita", "sem receita",
|
||||||
|
"combate bactérias", "elimina fungos", "trata pele",
|
||||||
|
"arma", "munição", "bala", "revólver", "pistola", "cartucho",
|
||||||
|
"espingarda", "carabina", "fuzil", "metralhadora",
|
||||||
|
"explosivo", "bomba", "dinamite", "pólvora",
|
||||||
|
"droga", "entorpecente", "haixe", "maconha", "cocaine", "crack",
|
||||||
|
"heroína", " LSD", "ecstasy", "MDMA", "substância controlada", "psicotrópico",
|
||||||
|
"lençol motel", "uso adulto", "18+", "erótico",
|
||||||
|
"animais silvestres", "fauna nativa", "ave silvestre",
|
||||||
|
"tartaruga marinha", "mico-leão", "peixe ornamental",
|
||||||
|
}
|
||||||
|
|
||||||
|
PROHIBITED_SHOPEE = {
|
||||||
|
" original", " autêntico", "genuíno", "original de verdade",
|
||||||
|
"garantia de originalidade",
|
||||||
|
"melhor que", "superior a", "mais barato que", "pior que",
|
||||||
|
"comparado a", "na frente de",
|
||||||
|
"frete grátis", "frete gratuito",
|
||||||
|
"promoção", "desconto especial", "oferta exclusiva", "liquidação",
|
||||||
|
"Deus", "Jesus", "abençoado", "salvação",
|
||||||
|
"Bolsonaro", "Lula", "comunismo", "fascismo", "ESG", "woke",
|
||||||
|
"R$ 1", "R$ 0,01", "de R$ 999 por R$ 1",
|
||||||
|
"clube do preço baixo", "grupo de revenda", "compre junto e ganhe", "kit revenda",
|
||||||
|
}
|
||||||
|
|
||||||
|
PROHIBITED_AMAZON = {
|
||||||
|
"made in China", "veio da China", "fabricado na China",
|
||||||
|
"chinês", # como insulto ou desmerecimento
|
||||||
|
"better than", "superior a", "worse than",
|
||||||
|
"best", "top 1", "número 1",
|
||||||
|
"mais vendido do Brasil",
|
||||||
|
"garantia vitalícia", "garantia de 10 anos", "garantia permanente",
|
||||||
|
"só hoje", "últimasunidades", "última peça", "último par", "hoje ou nunca",
|
||||||
|
"5 estrelas", "avaliado por dermatologistas", "clinicamente testado",
|
||||||
|
}
|
||||||
|
|
||||||
|
ALL_PROHIBITED = {
|
||||||
|
"MercadoLivre": PROHIBITED_ML,
|
||||||
|
"Shopee": PROHIBITED_SHOPEE,
|
||||||
|
"Amazon": PROHIBITED_AMAZON,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 2. CATEGORIAS REGULATÓRIAS — palavras-chave
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
ANVISA_COSMETICO = {
|
||||||
|
"protetor solar", "filtro solar", "fps", "creme para rosto", "creme para corpo",
|
||||||
|
"anti-idade", "clareador", "hidratante", "nutritivo",
|
||||||
|
"base", "batom", "sombra", "delineador", "máscara para cílios",
|
||||||
|
"perfume", "colônia", "tintura", "esmalte", "unha", "depilatório",
|
||||||
|
"desodorante", "shampoo", "condicionador", "queratina", "pantenol",
|
||||||
|
"maquiagem", "cosmético", "skincare", "serum",
|
||||||
|
}
|
||||||
|
|
||||||
|
ANVISA_SUPLEMENTO = {
|
||||||
|
"whey", "caseína", "suplemento proteico", "termogênico",
|
||||||
|
"carnitina", "guaraná", "efedrina", "sinefrina", "capsicum",
|
||||||
|
"vitamina", "mineral", "comprimido", "cápsula",
|
||||||
|
"ômega 3", "probiótico", "nutracêutico", "nutraceutico",
|
||||||
|
"emagrecedor", "bloqueador de gordura", "orlistat", "quitosana",
|
||||||
|
"energético", "bcaa", "glutamina", "creatina", "colágeno",
|
||||||
|
"suplemento", "anabolizante", "esteroides",
|
||||||
|
}
|
||||||
|
|
||||||
|
ANVISA_SANEANTE = {
|
||||||
|
"desinfetante", "sanitizante", "alvejante", "detergente bactericida",
|
||||||
|
"produto de limpeza", "limpeza", "antimicrobiano", "virucida",
|
||||||
|
"comodoro", "q-boa", "Veja",
|
||||||
|
}
|
||||||
|
|
||||||
|
MAPA_ANIMAL = {
|
||||||
|
"mel", "própolis", "geleia real", "pólen",
|
||||||
|
"queijo", "laticínio", "leite", "manteiga", "nata",
|
||||||
|
"salsicha", "linguiça", "presunto", "bacon", "salame", "embutido",
|
||||||
|
"carne processada", "hambúrguer", "almôndega",
|
||||||
|
"ovo", "ovos",
|
||||||
|
"peixe", "pescado", "sardinha", "filé de peixe",
|
||||||
|
"geleia com mel", "geleia com animal",
|
||||||
|
}
|
||||||
|
|
||||||
|
MAPA_VEGETAL = {
|
||||||
|
"café torrado", "café moído", "óleo de soja", "óleo de canola",
|
||||||
|
"óleo de girassol", "óleo vegetal",
|
||||||
|
"arroz parboilizado", "feijão industrializado",
|
||||||
|
"erva medicinal", "chá medicinal",
|
||||||
|
"orgânico", "produto orgânico",
|
||||||
|
}
|
||||||
|
|
||||||
|
INMETRO_BRINQUEDO = {
|
||||||
|
"brinquedo", "brinq", "boneca", "boneco", "carro", "avião",
|
||||||
|
"jogo infantil", "quebra-cabeça", "puzzle", "monta",
|
||||||
|
"bola", "roda", "infantil", "criança",
|
||||||
|
}
|
||||||
|
|
||||||
|
INMETRO_PUERICULTURA = {
|
||||||
|
"cadeirinha auto", "berço", "carrinho de bebê", "alcofa",
|
||||||
|
"moisés", "porta-bebê", "bebê conforto",
|
||||||
|
}
|
||||||
|
|
||||||
|
INMETRO_ELETRICO = {
|
||||||
|
"filtro de linha", "estabilizador", "nobreak", "no-break",
|
||||||
|
"lâmpada led", "lampada led", "led",
|
||||||
|
"capacete", "capacete de moto", "capacete motociclista",
|
||||||
|
"colchão", "extintor",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 3. PRODUTOS SEMPRE PROIBIDOS (não vender)
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
ALWAYS_BLOCK = {
|
||||||
|
"cigarro eletrônico", "vaper", "vaporizador",
|
||||||
|
"esteroides", "anabolizante",
|
||||||
|
"thc", "maconha", "haixe",
|
||||||
|
"armamento", "arma de fogo", "munição",
|
||||||
|
"explosivo", "bomba",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 4. FUNÇÕES AUXILIARES
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def normalize(text: str) -> str:
|
||||||
|
"""Remove acentos e converte para minúsculas."""
|
||||||
|
text = text.lower()
|
||||||
|
text = unicodedata.normalize("NFD", text)
|
||||||
|
text = re.sub(r"[\u0300-\u036f]", "", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def strip_punctuation(text: str) -> str:
|
||||||
|
"""Remove pontuação para busca parcial."""
|
||||||
|
return re.sub(r"[^\w\s]", " ", text)
|
||||||
|
|
||||||
|
|
||||||
|
def check_wordlists(text: str, wordlist: set, label: str) -> list[dict]:
|
||||||
|
"""Retorna violações de uma wordlist."""
|
||||||
|
violations = []
|
||||||
|
text_norm = normalize(text)
|
||||||
|
text_clean = strip_punctuation(text_norm)
|
||||||
|
for word in wordlist:
|
||||||
|
word_norm = normalize(word)
|
||||||
|
# Match whole word
|
||||||
|
pattern = r"\b" + re.escape(word_norm) + r"\b"
|
||||||
|
if re.search(pattern, text_clean):
|
||||||
|
violations.append({
|
||||||
|
"word": word,
|
||||||
|
"list": label,
|
||||||
|
"severity": "HIGH",
|
||||||
|
})
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def detect_category(text: str) -> dict[str, bool]:
|
||||||
|
"""Heurística simples para detectar categoria regulatória."""
|
||||||
|
text_lower = text.lower()
|
||||||
|
text_norm = normalize(text_lower)
|
||||||
|
text_clean = strip_punctuation(text_norm)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"anvisa_cosmetico": any(k in text_clean for k in [normalize(k) for k in ANVISA_COSMETICO]),
|
||||||
|
"anvisa_suplemento": any(k in text_clean for k in [normalize(k) for k in ANVISA_SUPLEMENTO]),
|
||||||
|
"anvisa_saneante": any(k in text_clean for k in [normalize(k) for k in ANVISA_SANEANTE]),
|
||||||
|
"mapa_animal": any(k in text_clean for k in [normalize(k) for k in MAPA_ANIMAL]),
|
||||||
|
"mapa_vegetal": any(k in text_clean for k in [normalize(k) for k in MAPA_VEGETAL]),
|
||||||
|
"inmetro_brinquedo": any(k in text_clean for k in [normalize(k) for k in INMETRO_BRINQUEDO]),
|
||||||
|
"inmetro_puericultura": any(k in text_clean for k in [normalize(k) for k in INMETRO_PUERICULTURA]),
|
||||||
|
"inmetro_eletrico": any(k in text_clean for k in [normalize(k) for k in INMETRO_ELETRICO]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_always_block(text: str) -> list[dict]:
|
||||||
|
"""Verifica produtos sempre bloqueados."""
|
||||||
|
violations = []
|
||||||
|
text_clean = normalize(strip_punctuation(normalize(text)))
|
||||||
|
for term in ALWAYS_BLOCK:
|
||||||
|
term_norm = normalize(term)
|
||||||
|
pattern = r"\b" + re.escape(term_norm) + r"\b"
|
||||||
|
if re.search(pattern, text_clean):
|
||||||
|
violations.append({
|
||||||
|
"word": term,
|
||||||
|
"list": "ALWAYS_BLOCK",
|
||||||
|
"severity": "CRITICAL",
|
||||||
|
})
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 5. SCORING
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
WEIGHTS = {
|
||||||
|
"ALWAYS_BLOCK": -100, # sempre bloqueia
|
||||||
|
"MercadoLivre": -8,
|
||||||
|
"Shopee": -8,
|
||||||
|
"Amazon": -8,
|
||||||
|
"anvisa_cosmetico": -5,
|
||||||
|
"anvisa_suplemento": -5,
|
||||||
|
"anvisa_saneante": -5,
|
||||||
|
"mapa_animal": -5,
|
||||||
|
"mapa_vegetal": -5,
|
||||||
|
"inmetro_brinquedo": -5,
|
||||||
|
"inmetro_puericultura": -5,
|
||||||
|
"inmetro_eletrico": -5,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_score(violations: list[dict], detected_categories: dict) -> dict:
|
||||||
|
"""Calcula score 0-100."""
|
||||||
|
score = 100
|
||||||
|
|
||||||
|
for v in violations:
|
||||||
|
key = v.get("list", "unknown")
|
||||||
|
if key in WEIGHTS:
|
||||||
|
score += WEIGHTS[key] # já são negativos
|
||||||
|
|
||||||
|
# Penalidades por categoria detectada (sem registro declarado)
|
||||||
|
for cat, detected in detected_categories.items():
|
||||||
|
if detected and cat in WEIGHTS:
|
||||||
|
score += WEIGHTS[cat]
|
||||||
|
|
||||||
|
score = max(0, min(100, score))
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
def decision(score: int) -> str:
|
||||||
|
if score >= 90:
|
||||||
|
return "PUBLICAR"
|
||||||
|
elif score >= 70:
|
||||||
|
return "REVISAR"
|
||||||
|
elif score >= 40:
|
||||||
|
return "REVISAR_OBRIGATORIO"
|
||||||
|
else:
|
||||||
|
return "BLOQUEADO"
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 6. RECOMENDAÇÕES
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
RECOMMENDATIONS = {
|
||||||
|
"anvisa_cosmetico": "⚠️ Cosmético — exigir número de Registro ANVISA antes de publicar (RDC 685/2018)",
|
||||||
|
"anvisa_suplemento": "⚠️ Suplemento — exigir número de Registro ANVISA antes de publicar (RDC 26/2015)",
|
||||||
|
"anvisa_saneante": "⚠️ Saneante — exigir registro ANVISA antes de publicar",
|
||||||
|
"mapa_animal": "⚠️ Produto animal — exige SIF (venda interestadual) ou SIE (venda estadual). Verificar com MAPA.",
|
||||||
|
"mapa_vegetal": "⚠️ Produto vegetal processado — pode exigir registro MAPA ou estadual.",
|
||||||
|
"inmetro_brinquedo": "⚠️ Brinquedo — exige certificação INMETRO compulsória.",
|
||||||
|
"inmetro_puericultura": "⚠️ Artigo de puericultura — exige certificação INMETRO.",
|
||||||
|
"inmetro_eletrico": "⚠️ Equipamento elétrico — exige certificação de segurança INMETRO.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 7. MAIN
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_check(title: str, description: str = "", category_hint: str = "") -> dict:
|
||||||
|
"""
|
||||||
|
Verifica título e descrição contra todas as listas.
|
||||||
|
Retorna dict JSON com score, violações e recomendações.
|
||||||
|
"""
|
||||||
|
full_text = f"{title} {description}"
|
||||||
|
|
||||||
|
# Coleta violações
|
||||||
|
all_violations = []
|
||||||
|
|
||||||
|
for marketplace, wordlist in ALL_PROHIBITED.items():
|
||||||
|
all_violations.extend(check_wordlists(full_text, wordlist, marketplace))
|
||||||
|
|
||||||
|
all_violations.extend(check_always_block(full_text))
|
||||||
|
|
||||||
|
# Detecta categorias
|
||||||
|
detected = detect_category(full_text)
|
||||||
|
|
||||||
|
# Se categoria foi informada manualmente
|
||||||
|
if category_hint:
|
||||||
|
cat_map = {
|
||||||
|
"cosmetico": "anvisa_cosmetico",
|
||||||
|
"suplemento": "anvisa_suplemento",
|
||||||
|
"saneante": "anvisa_saneante",
|
||||||
|
"animal": "mapa_animal",
|
||||||
|
"vegetal": "mapa_vegetal",
|
||||||
|
"brinquedo": "inmetro_brinquedo",
|
||||||
|
"puericultura": "inmetro_puericultura",
|
||||||
|
"eletrico": "inmetro_eletrico",
|
||||||
|
}
|
||||||
|
hint_key = cat_map.get(category_hint.lower().strip())
|
||||||
|
if hint_key:
|
||||||
|
detected[hint_key] = True
|
||||||
|
|
||||||
|
# Score
|
||||||
|
score = compute_score(all_violations, detected)
|
||||||
|
dec = decision(score)
|
||||||
|
|
||||||
|
# Recomendações
|
||||||
|
recommendations = []
|
||||||
|
for cat, active in detected.items():
|
||||||
|
if active and cat in RECOMMENDATIONS:
|
||||||
|
recommendations.append(RECOMMENDATIONS[cat])
|
||||||
|
|
||||||
|
# Violações críticas
|
||||||
|
critical = [v for v in all_violations if v.get("severity") == "CRITICAL"]
|
||||||
|
if critical:
|
||||||
|
recommendations.insert(0, "🚫 PRODUTO SEMPRE BLOQUEADO — Não publar. Verificar se é produto proibido (armas, drogas, cigarros eletrônicos, etc.)")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"score": score,
|
||||||
|
"decision": dec,
|
||||||
|
"violations": all_violations,
|
||||||
|
"violation_count": len(all_violations),
|
||||||
|
"detected_categories": {k: v for k, v in detected.items() if v},
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"check_passed": dec in ("PUBLICAR", "REVISAR"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def interactive():
|
||||||
|
print("=== COMPLIANCE CHECKER — Ecommerce M5 Brasil ===\n")
|
||||||
|
print("Pressione ENTER para pular qualquer campo.\n")
|
||||||
|
|
||||||
|
title = input("Título do produto: ").strip()
|
||||||
|
if not title:
|
||||||
|
print("Título é obrigatório. Encerrando.")
|
||||||
|
return
|
||||||
|
|
||||||
|
description = input("Descrição do produto (ou ENTER para vazio): ").strip()
|
||||||
|
category = input(
|
||||||
|
"Categoria (opcional — ajuda na detecção):\n"
|
||||||
|
" cosmetico | suplemento | saneante | animal | vegetal\n"
|
||||||
|
" brinquedo | puericultura | eletrico | ENTER=pular\n"
|
||||||
|
" >> "
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
result = run_check(title, description, category)
|
||||||
|
print("\n" + json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
if "--interactive" in args or len(args) == 0:
|
||||||
|
interactive()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
title = ""
|
||||||
|
description = ""
|
||||||
|
category = ""
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
if args[i] in ("--title", "-t"):
|
||||||
|
title = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
elif args[i] in ("--description", "-d"):
|
||||||
|
description = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
elif args[i] in ("--category", "-c"):
|
||||||
|
category = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
elif args[i] == "--interactive":
|
||||||
|
interactive()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
print("Erro: --title é obrigatório.", file=sys.stderr)
|
||||||
|
print("Uso: python3 compliance-checker.py --title '...' [--description '...'] [--category 'cosmetico'] [--interactive]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
result = run_check(title, description, category)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
304
scripts/docker-compose.m5.yml
Normal file
304
scripts/docker-compose.m5.yml
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
# docker-compose.m5.yml — Motor Ecommerce M5 Brasil
|
||||||
|
# Versão atualizada conforme manual Saleor 3.23
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ── Traefik (Proxy Reverso) ─────────────────────────
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.0
|
||||||
|
container_name: m5_traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
- --certificatesresolvers.myresolver.acme.httpchallenge=true
|
||||||
|
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
|
||||||
|
- --certificatesresolvers.myresolver.acme.email=irisflorencio@yahoo.com.br
|
||||||
|
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "8090:8080"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- traefik-acme:/letsencrypt
|
||||||
|
- ./traefik-conf/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./traefik-conf/routes.yml:/etc/traefik/routes.yml:ro
|
||||||
|
- /etc/letsencrypt:/certs:ro
|
||||||
|
- ./traefik-conf/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./traefik-conf/routes.yml:/etc/traefik/routes.yml:ro
|
||||||
|
- /etc/letsencrypt:/certs:ro
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
# ── Banco de dados principal ──────────────────────────
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: m5_postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-m5ecommerce}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-m5user}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-m5user}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# ── Cache / Session ──────────────────────────────────
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: m5_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# ── Saleor API ───────────────────────────────────────
|
||||||
|
saleor:
|
||||||
|
image: ghcr.io/saleor/saleor:3.23
|
||||||
|
container_name: m5_saleor
|
||||||
|
restart: unless-stopped
|
||||||
|
entrypoint: ["/app/entrypoint.sh"]
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.saleor-api.rule=Host(`loja.m5digital.net.br`)"
|
||||||
|
- "traefik.http.routers.saleor-api.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.saleor-api.tls=true"
|
||||||
|
- "traefik.http.services.saleor-api.loadbalancer.server.port=8000"
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-m5ecommerce}
|
||||||
|
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
|
||||||
|
- CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
|
||||||
|
- SECRET_KEY=${SALEOR_SECRET_KEY}
|
||||||
|
- ALLOWED_HOSTS=loja.m5digital.net.br,localhost,127.0.0.1,dashboard.m5digital.net.br
|
||||||
|
- ALLOWED_GRAPHQL_ORIGINS=https://dashboard.m5digital.net.br,https://loja.m5digital.net.br,http://localhost:3000
|
||||||
|
- DEFAULT_MAIL_FROM=${DEFAULT_EMAIL_FROM}
|
||||||
|
- EMAIL_URL=${EMAIL_URL}
|
||||||
|
- RSA_PRIVATE_KEY=/app/media/RSA_PRIVATE_KEY.pem
|
||||||
|
- STRIPE_API_KEY=${STRIPE_API_KEY}
|
||||||
|
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
||||||
|
- MERCADOPAGO_ACCESS_TOKEN=${MERCADOPAGO_ACCESS_TOKEN}
|
||||||
|
- MERCADOPAGO_WEBHOOK_SECRET=${MERCADOPAGO_WEBHOOK_SECRET}
|
||||||
|
volumes:
|
||||||
|
- ./saleor-media:/app/media
|
||||||
|
- ./entrypoint.sh:/app/entrypoint.sh:ro
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
|
||||||
|
# ── Saleor Worker (Celery) ─────────────────────────
|
||||||
|
worker:
|
||||||
|
image: ghcr.io/saleor/saleor:3.23
|
||||||
|
container_name: m5_saleor_worker
|
||||||
|
restart: unless-stopped
|
||||||
|
command: celery -A saleor worker --loglevel=info
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-m5ecommerce}
|
||||||
|
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
|
||||||
|
- CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
|
||||||
|
- SECRET_KEY=${SALEOR_SECRET_KEY}
|
||||||
|
volumes:
|
||||||
|
- saleor-media:/app/media
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
# ── Saleor Dashboard ─────────────────────────────────
|
||||||
|
saleor-dashboard:
|
||||||
|
image: ghcr.io/saleor/saleor-dashboard:latest
|
||||||
|
container_name: m5_saleor_dashboard
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- API_URL=https://loja.m5digital.net.br/graphql/
|
||||||
|
- APP_MOUNT_URI=/
|
||||||
|
- NODE_ENV=production
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.saleor-dash.rule=Host(`dashboard.m5digital.net.br`)"
|
||||||
|
- "traefik.http.routers.saleor-dash.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.saleor-dash.tls=true"
|
||||||
|
- "traefik.http.services.saleor-dash.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
# ── n8n (Automação) ──────────────────────────────────
|
||||||
|
n8n:
|
||||||
|
image: n8nio/n8n:${N8N_VERSION:-1.70}
|
||||||
|
container_name: m5_n8n
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
- DB_TYPE=postgresdb
|
||||||
|
- DB_POSTGRESDB_HOST=postgres
|
||||||
|
- DB_POSTGRESDB_PORT=5432
|
||||||
|
- DB_POSTGRESDB_DATABASE=${N8N_DB:-n8n}
|
||||||
|
- DB_POSTGRESDB_USER=${POSTGRES_USER:-m5user}
|
||||||
|
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- N8N_BASIC_AUTH_ACTIVE=true
|
||||||
|
- N8N_BASIC_AUTH_USER=${N8N_USER}
|
||||||
|
- N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
|
||||||
|
- N8N_HOST=https://n8n.m5digital.net.br
|
||||||
|
- WEBHOOK_URL=https://n8n.m5digital.net.br/
|
||||||
|
- N8N_PROTOCOL=https
|
||||||
|
- N8N_TRUST_PROXY=true
|
||||||
|
- GENERIC_TIMEZONE=America/Sao_Paulo
|
||||||
|
- WA_AUTOCARRIER_URL=http://m5_openwa:8080
|
||||||
|
- SALEOR_API_URL=https://loja.m5digital.net.br/graphql/
|
||||||
|
volumes:
|
||||||
|
- n8n_data:/home/node/.n8n
|
||||||
|
- ./n8n-workflows:/workflows
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.n8n.rule=Host(`n8n.m5digital.net.br`)"
|
||||||
|
- "traefik.http.routers.n8n.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.n8n.tls=true"
|
||||||
|
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
|
||||||
|
|
||||||
|
# ── OpenWA (WhatsApp) ────────────────────────────────
|
||||||
|
openwa:
|
||||||
|
image: ghcr.io/rmyndharis/openwa:latest
|
||||||
|
container_name: m5_openwa
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
- PUPPETEER_EXECUTABLE_PATH=/opt/google/chrome/chrome
|
||||||
|
- CHROME_PATH=/opt/google/chrome/chrome
|
||||||
|
- WA_CHROMIUM_FLAGS=--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --no-zygote --single-process
|
||||||
|
- WA_USE_CHROME=true
|
||||||
|
- MULTI_DEVICE=true
|
||||||
|
- READ_MESSAGES=true
|
||||||
|
- GROUP_PARTICIPANTS=true
|
||||||
|
- WEBHOOK_URL=${WA_WEBHOOK_URL:-https://n8n.m5digital.net.br/webhook/wa}
|
||||||
|
env_file:
|
||||||
|
- ./openwa.env
|
||||||
|
ports:
|
||||||
|
- "2785:2785"
|
||||||
|
- "8080:8080"
|
||||||
|
- "8081:8081"
|
||||||
|
- "8002:8002"
|
||||||
|
volumes:
|
||||||
|
- openwa_data:/var/lib/wa-automate
|
||||||
|
- ./openwa-sessions:/sessions
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
security_opt:
|
||||||
|
- seccomp=unconfined
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.openwa.rule=Host(`wa.m5digital.net.br`)"
|
||||||
|
- "traefik.http.routers.openwa.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.openwa.tls=true"
|
||||||
|
- "traefik.http.services.openwa.loadbalancer.server.port=2785"
|
||||||
|
|
||||||
|
# ── Gitea (Git) ─────────────────────────────────────
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea:${GITEA_VERSION:-1.22}
|
||||||
|
container_name: m5_gitea
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "2222:22"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
- GITEA__database__DB_TYPE=postgres
|
||||||
|
- GITEA__database__HOST=postgres:5432
|
||||||
|
- GITEA__database__NAME=${GITEA_DB:-gitea}
|
||||||
|
- GITEA__database__USER=${POSTGRES_USER:-m5user}
|
||||||
|
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
|
||||||
|
- GITEA__server__PROTOCOL=http
|
||||||
|
- GITEA__server__ROOT_URL=https://git.m5digital.net.br/
|
||||||
|
- GITEA__server__DOMAIN=git.m5digital.net.br
|
||||||
|
- GITEA__security__INSTALL_LOCK=true
|
||||||
|
- GITEA__service__DISABLE_REGISTRATION=false
|
||||||
|
volumes:
|
||||||
|
- gitea_data:/data
|
||||||
|
- ./gitea-repos:/git/repositories
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.gitea.rule=Host(`git.m5digital.net.br`)"
|
||||||
|
- "traefik.http.routers.gitea.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gitea.tls=true"
|
||||||
|
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
# ── yt-pub-livesx ──────────────────────────────────
|
||||||
|
yt-pub-livesx-dashboard:
|
||||||
|
image: yt-pub-livesx-dashboard
|
||||||
|
container_name: yt-pub-livesx-dashboard-1
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=America/Sao_Paulo
|
||||||
|
ports:
|
||||||
|
- "8091:8091"
|
||||||
|
volumes:
|
||||||
|
- ./yt-pub-livesx/data:/app/data
|
||||||
|
- ./yt-pub-livesx/credentials.enc:/app/credentials.enc:ro
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.video.rule=Host(`video.m5digital.net.br`)"
|
||||||
|
- "traefik.http.routers.video.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.video.tls=true"
|
||||||
|
- "traefik.http.services.video.loadbalancer.server.port=8091"
|
||||||
|
|
||||||
|
yt-pub-livesx-scheduler:
|
||||||
|
image: yt-pub-livesx-scheduler
|
||||||
|
container_name: yt-pub-livesx-scheduler-1
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=America/Sao_Paulo
|
||||||
|
volumes:
|
||||||
|
- ./yt-pub-livesx/data:/app/data
|
||||||
|
- ./yt-pub-livesx/credentials.enc:/app/credentials.enc:ro
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
m5_network:
|
||||||
|
name: scripts_m5_network
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
saleor-media:
|
||||||
|
gitea_data:
|
||||||
|
n8n_data:
|
||||||
|
openwa_data:
|
||||||
|
traefik-acme:
|
||||||
30
scripts/entrypoint.sh
Executable file
30
scripts/entrypoint.sh
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Ler RSA key do arquivo ou da variável de ambiente
|
||||||
|
if [ -n "$RSA_PRIVATE_KEY" ]; then
|
||||||
|
if [ -f "$RSA_PRIVATE_KEY" ]; then
|
||||||
|
# É um caminho de arquivo - ler o conteúdo
|
||||||
|
echo "Lendo RSA key do arquivo: $RSA_PRIVATE_KEY"
|
||||||
|
RSA_PRIVATE_KEY_CONTENT=$(cat "$RSA_PRIVATE_KEY")
|
||||||
|
export RSA_PRIVATE_KEY="$RSA_PRIVATE_KEY_CONTENT"
|
||||||
|
elif [[ "$RSA_PRIVATE_KEY" == *-----BEGIN* ]]; then
|
||||||
|
echo "RSA key já está inline"
|
||||||
|
else
|
||||||
|
# Provavelmente base64 encoded
|
||||||
|
echo "Decodificando RSA key base64..."
|
||||||
|
python3 -c "
|
||||||
|
import base64, os
|
||||||
|
key = os.environ.get('RSA_PRIVATE_KEY', '')
|
||||||
|
if key:
|
||||||
|
with open('/tmp/RSA.pem', 'wb') as f:
|
||||||
|
f.write(base64.b64decode(key))
|
||||||
|
os.chmod('/tmp/RSA.pem', 0o600)
|
||||||
|
"
|
||||||
|
if [ -f /tmp/RSA.pem ]; then
|
||||||
|
RSA_PRIVATE_KEY_CONTENT=$(cat /tmp/RSA.pem)
|
||||||
|
export RSA_PRIVATE_KEY="$RSA_PRIVATE_KEY_CONTENT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec uvicorn saleor.asgi:application --host 0.0.0.0 --port 8000 --workers 2 --timeout-keep-alive 35
|
||||||
5
scripts/health-check.sh
Executable file
5
scripts/health-check.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# M5 - Health Check Docker
|
||||||
|
# Data: 24/06/2026
|
||||||
|
echo "Health Check - $(date)"
|
||||||
|
# Será implementado com docker ps/health
|
||||||
55
scripts/import-chatwoot-workflow.sh
Normal file
55
scripts/import-chatwoot-workflow.sh
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Importar Chatwoot Workflow para n8n
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
N8N_URL="https://n8n.m5digital.net.br"
|
||||||
|
N8N_USER="admin"
|
||||||
|
N8N_PASSWORD="lKs9DA7S_PL7xv-YrQzi7eFmC8CcGKPrpceGxOqnYOY"
|
||||||
|
WORKFLOW_FILE="/root/ecommerce/workflows/chatwoot-integration.json"
|
||||||
|
|
||||||
|
echo "🔄 Importando Chatwoot Workflow..."
|
||||||
|
|
||||||
|
# 1. Fazer login
|
||||||
|
JWT_TOKEN=$(curl -s -X POST "${N8N_URL}/webhook-test" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"email\":\"${N8N_USER}\",\"password\":\"${N8N_PASSWORD}\"}" | \
|
||||||
|
jq -r '.data.result.data.jwt')
|
||||||
|
|
||||||
|
if [ "$JWT_TOKEN" = "null" ]; then
|
||||||
|
echo "❌ Falha ao fazer login no n8n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Importar workflow
|
||||||
|
RESPONSE=$(curl -s -X POST "${N8N_URL}/workflows/import" \
|
||||||
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"workflowData\":$(cat ${WORKFLOW_FILE})}")
|
||||||
|
|
||||||
|
# 3. Verificar resultado
|
||||||
|
WORKFLOW_ID=$(echo "$RESPONSE" | jq -r '.workflow.id')
|
||||||
|
if [ "$WORKFLOW_ID" != "null" ]; then
|
||||||
|
echo "✅ Workflow importado com sucesso!"
|
||||||
|
echo "ID: ${WORKFLOW_ID}"
|
||||||
|
echo "URL: ${N8N_URL}/workflows/${WORKFLOW_ID}"
|
||||||
|
else
|
||||||
|
echo "❌ Falha ao importar workflow"
|
||||||
|
echo "$RESPONSE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Ativar webhook no Chatwoot
|
||||||
|
echo -e "\n🔧 Configurando webhook no Chatwoot..."
|
||||||
|
|
||||||
|
WEBHOOK_PAYLOAD='{
|
||||||
|
"url": "https://n8n.m5digital.net.br/webhook/chatwoot",
|
||||||
|
"events": ["conversation.created", "message.created"],
|
||||||
|
"secret": "M5ChatwootSecret2026"
|
||||||
|
}'
|
||||||
|
|
||||||
|
curl -s -X POST "https://chat.m5digital.net.br/api/v1/accounts/1/webhooks" \
|
||||||
|
-H "api_access_token: AwnRiNkenTfzmipP8sM4dewQ" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$WEBHOOK_PAYLOAD" | jq -r '.payload.id // .error'
|
||||||
|
|
||||||
|
echo -e "\n✅ Integração configurada!"
|
||||||
110
scripts/insert_workflow.py
Normal file
110
scripts/insert_workflow.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
WORKFLOW_ID = "fa489bed-a0ac-4856-a38e-e78bed2adf9d"
|
||||||
|
WEBHOOK_ID = "whatsapp-messages"
|
||||||
|
PROJECT_ID = "NHIYHwVpHURSGRFV"
|
||||||
|
|
||||||
|
nodes = [
|
||||||
|
{
|
||||||
|
"parameters": {"httpMethod": "POST", "path": "whatsapp-messages", "responseMode": "lastNode", "options": {}},
|
||||||
|
"id": "wh1", "name": "Webhook WhatsApp", "type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2, "position": [250, 300], "webhookId": WEBHOOK_ID
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {"jsCode": "const d=$input.item.json;const e=d.event||'';const m=d.data||{};const t=m.hasMedia?'media':m.body?'text':e.includes('session')?'session':'other';return[{json:{...d,messageType:t}}];"},
|
||||||
|
"id": "cl1", "name": "Classificar Mensagem", "type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2, "position": [500, 300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {"jsCode": "console.log(JSON.stringify($input.item.json));return $input.all();"},
|
||||||
|
"id": "db1", "name": "Log Mensagem", "type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2, "position": [750, 300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {"jsCode": "return[{json:{received:true,timestamp:new Date().toISOString()}}];"},
|
||||||
|
"id": "ok1", "name": "Responder 200", "type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2, "position": [1000, 300]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
connections = {
|
||||||
|
"Webhook WhatsApp": {"main": [[{"node": "Classificar Mensagem", "type": "main", "index": 0}]]},
|
||||||
|
"Classificar Mensagem": {"main": [[{"node": "Log Mensagem", "type": "main", "index": 0}]]},
|
||||||
|
"Log Mensagem": {"main": [[{"node": "Responder 200", "type": "main", "index": 0}]]}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = {"executionOrder": "v1", "saveManualExecutions": True, "callerPolicy": "workflowsFromSameOwner"}
|
||||||
|
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host="m5_postgres",
|
||||||
|
dbname="n8n",
|
||||||
|
user="m5user",
|
||||||
|
password="YXnJMMqAZ5uZrFj2lMo_AnhEmQq4uVoCfOuXaG7pU0Y",
|
||||||
|
port=5432
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Insert workflow
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO workflow_entity (
|
||||||
|
name, active, nodes, connections, settings, staticData, pinData,
|
||||||
|
versionId, triggerCount, id, meta, parentFolderId, isArchived, versionCounter,
|
||||||
|
description, activeVersionId, createdAt, updatedAt
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, NULL, NULL,
|
||||||
|
%s, %s, %s, NULL, NULL, %s, %s,
|
||||||
|
%s, NULL, NOW(), NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
active = EXCLUDED.active,
|
||||||
|
nodes = EXCLUDED.nodes,
|
||||||
|
connections = EXCLUDED.connections,
|
||||||
|
settings = EXCLUDED.settings,
|
||||||
|
updatedAt = NOW()
|
||||||
|
""", (
|
||||||
|
"WhatsApp Messages - OpenWA",
|
||||||
|
True,
|
||||||
|
json.dumps(nodes),
|
||||||
|
json.dumps(connections),
|
||||||
|
json.dumps(settings),
|
||||||
|
"1", 1, WORKFLOW_ID,
|
||||||
|
False, 1,
|
||||||
|
"Recebe webhooks do OpenWA e salva no banco"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Insert webhook entity
|
||||||
|
wh_entity_id = str(uuid.uuid4())
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO webhook_entity (
|
||||||
|
id, webhookId, workflowId, method, path, node, conditions, position,
|
||||||
|
retries, timeout, keywords, redactUrl, redactHeaders, redactBody, createdAt
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, %s, NULL, %s,
|
||||||
|
3, NULL, NULL, false, NULL, false, NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (webhookId) DO UPDATE SET
|
||||||
|
workflowId = EXCLUDED.workflowId,
|
||||||
|
updatedAt = NOW()
|
||||||
|
""", (
|
||||||
|
wh_entity_id, WEBHOOK_ID, WORKFLOW_ID,
|
||||||
|
"POST", "/webhook/whatsapp-messages", "Webhook WhatsApp",
|
||||||
|
json.dumps([250, 300])
|
||||||
|
))
|
||||||
|
|
||||||
|
# Insert shared_workflow
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO shared_workflow (workflowId, projectId, role, createdAt, updatedAt)
|
||||||
|
VALUES (%s, %s, %s, NOW(), NOW())
|
||||||
|
ON CONFLICT (workflowId, projectId) DO NOTHING
|
||||||
|
""", (WORKFLOW_ID, PROJECT_ID, "workflow"))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print(f"Workflow criado: {WORKFLOW_ID}")
|
||||||
|
print(f"Webhook ID: {WEBHOOK_ID}")
|
||||||
|
print("Pronto!")
|
||||||
62
scripts/minio-compose.yml
Normal file
62
scripts/minio-compose.yml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: m5_minio
|
||||||
|
hostname: minio
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
ports:
|
||||||
|
- "9000:9000" # API
|
||||||
|
- "9001:9001" # Console
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin123}
|
||||||
|
MINIO_SITE_NAME: "M5 Digital MinIO"
|
||||||
|
MINIO_SITE_REGION: "us-east-1"
|
||||||
|
volumes:
|
||||||
|
- ./minio/data:/data
|
||||||
|
- ./minio/config:/root/.minio
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mc", "ready", "local"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc:latest
|
||||||
|
container_name: m5_minio_init
|
||||||
|
networks:
|
||||||
|
- m5_network
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c "
|
||||||
|
sleep 5;
|
||||||
|
mc alias set myminio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD};
|
||||||
|
mc anonymous set download myminio/public;
|
||||||
|
mc admin user add myminio ${MINIO_SERVICE_USER:-crmuser} ${MINIO_SERVICE_PASSWORD:-crmpass123};
|
||||||
|
mc admin policy attach myminio readwrite --user ${MINIO_SERVICE_USER:-crmuser};
|
||||||
|
mc mb myminio/hermes-crm --ignore-existing;
|
||||||
|
mc mb myminio/hermes-crm/avatars --ignore-existing;
|
||||||
|
mc mb myminio/hermes-crm/documents --ignore-existing;
|
||||||
|
mc mb myminio/hermes-crm/attachments --ignore-existing;
|
||||||
|
mc mb myminio/hermes-crm/backups --ignore-existing;
|
||||||
|
mc mb myminio/hermes-crm/exports --ignore-existing;
|
||||||
|
mc mb myminio/hermes-crm/temp --ignore-existing;
|
||||||
|
mc mb myminio/hermes-ecom --ignore-existing;
|
||||||
|
mc mb myminio/hermes-ecom/products --ignore-existing;
|
||||||
|
mc mb myminio/hermes-ecom/images --ignore-existing;
|
||||||
|
mc mb myminio/hermes-ecom/documents --ignore-existing;
|
||||||
|
echo 'MinIO initialized successfully';
|
||||||
|
exit 0;
|
||||||
|
"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
m5_network:
|
||||||
|
name: scripts_m5_network
|
||||||
|
external: true
|
||||||
67
scripts/openwa-compose.yml
Normal file
67
scripts/openwa-compose.yml
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# OpenWA v0.5.0 - Production compose adapted for m5 stack
|
||||||
|
# Base: docker-compose.dev.yml (rmyndharis/OpenWA)
|
||||||
|
|
||||||
|
services:
|
||||||
|
openwa:
|
||||||
|
image: ghcr.io/rmyndharis/openwa:latest
|
||||||
|
container_name: m5_openwa
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- 'no-new-privileges:true'
|
||||||
|
cap_add:
|
||||||
|
- SYS_ADMIN
|
||||||
|
- CHOWN
|
||||||
|
- DAC_OVERRIDE
|
||||||
|
- FOWNER
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
|
shm_size: '512mb'
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=2785
|
||||||
|
- HOME=/tmp
|
||||||
|
- XDG_CONFIG_HOME=/tmp/.config
|
||||||
|
- XDG_CACHE_HOME=/tmp/.cache
|
||||||
|
- DATABASE_TYPE=sqlite
|
||||||
|
- DATABASE_NAME=/app/data/openwa.sqlite
|
||||||
|
- DATABASE_SYNCHRONIZE=false
|
||||||
|
- DATABASE_LOGGING=false
|
||||||
|
- ENGINE_TYPE=whatsapp-web.js
|
||||||
|
- SESSION_DATA_PATH=/app/data/sessions
|
||||||
|
- PUPPETEER_HEADLESS=true
|
||||||
|
- PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu,--disable-software-rasterizer
|
||||||
|
- PUPPETEER_PROTOCOL_TIMEOUT=120000
|
||||||
|
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
- STORAGE_TYPE=local
|
||||||
|
- STORAGE_LOCAL_PATH=/app/data/media
|
||||||
|
- WEBHOOK_TIMEOUT=10000
|
||||||
|
- WEBHOOK_MAX_RETRIES=3
|
||||||
|
- WEBHOOK_RETRY_DELAY=5000
|
||||||
|
- QUEUE_ENABLED=false
|
||||||
|
- AUTO_START_SESSIONS=false
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
volumes:
|
||||||
|
- m5_openwa_data:/app/data
|
||||||
|
networks:
|
||||||
|
- scripts_m5_network
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'node', '-e', "require('http').get('http://localhost:2785/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 45s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.openwa.rule=Host(`wa.m5digital.net.br`)"
|
||||||
|
- "traefik.http.routers.openwa.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.openwa.tls=true"
|
||||||
|
- "traefik.http.routers.openwa.service=openwa"
|
||||||
|
- "traefik.http.services.openwa.loadbalancer.server.port=2785"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
m5_openwa_data:
|
||||||
|
name: m5_openwa_data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
scripts_m5_network:
|
||||||
|
external: true
|
||||||
3
scripts/openwa.env
Normal file
3
scripts/openwa.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu
|
||||||
|
PUPPETEER_HEADLESS=true
|
||||||
58
scripts/operacao_completa.sh
Executable file
58
scripts/operacao_completa.sh
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# AGENTE MESTRE - OPERAÇÃO COMPLETA
|
||||||
|
# Coordenar verificação, testes, integrações e documentação
|
||||||
|
|
||||||
|
echo "🚀 INICIANDO OPERAÇÃO COMPLETA - $(date)"
|
||||||
|
|
||||||
|
# 1. Verificar todas as instâncias
|
||||||
|
echo "🔍 Verificando instâncias..."
|
||||||
|
curl -s -o /dev/null -w "%{http_code} " https://crm5.m5digital.net.br
|
||||||
|
curl -s -o /dev/null -w "%{http_code} " https://chat.m5digital.net.br
|
||||||
|
curl -s -o /dev/null -w "%{http_code} " https://n8n.m5digital.net.br
|
||||||
|
curl -s -o /dev/null -w "%{http_code} " https://loja.m5digital.net.br
|
||||||
|
curl -s -o /dev/null -w "%{http_code} " https://dashboard.m5digital.net.br
|
||||||
|
curl -s -o /dev/null -w "%{http_code} " https://git.m5digital.net.br
|
||||||
|
curl -s -o /dev/null -w "%{http_code} " https://video.m5digital.net.br
|
||||||
|
|
||||||
|
# 2. Análise de logs
|
||||||
|
echo -e "\n📋 Analisando logs..."
|
||||||
|
docker ps --format "table {{.Names}}\t{{.Status}}"
|
||||||
|
docker ps -a --filter "status=exited" --format "table {{.Names}}\t{{.Status}}"
|
||||||
|
|
||||||
|
# 3. Testes de conectividade
|
||||||
|
echo -e "\n🌐 Testando conectividade..."
|
||||||
|
docker exec m5_traefik ping -c 2 chatwoot-chatwoot-1
|
||||||
|
docker exec m5_traefik ping -c 2 twenty_front
|
||||||
|
docker exec m5_traefik ping -c 2 m5_saleor
|
||||||
|
|
||||||
|
# 4. Verificar n8n workflows
|
||||||
|
echo -e "\n🔄 Verificando n8n..."
|
||||||
|
curl -s https://n8n.m5digital.net.br/api/v1/workflows | jq '.data[] | {id, name, active}' 2>/dev/null || echo "n8n não acessível"
|
||||||
|
|
||||||
|
# 5. Gerar relatório
|
||||||
|
echo -e "\n📊 Gerando relatório..."
|
||||||
|
cat > /tmp/relatorio_operacao.txt << EOF
|
||||||
|
RELATÓRIO DE OPERAÇÃO COMPLETA - $(date)
|
||||||
|
|
||||||
|
INSTÂNCIAS:
|
||||||
|
- Twenty CRM: https://crm5.m5digital.net.br
|
||||||
|
- Chatwoot: https://chat.m5digital.net.br
|
||||||
|
- n8n: https://n8n.m5digital.net.br
|
||||||
|
- Saleor: https://loja.m5digital.net.br
|
||||||
|
- Traefik: https://dashboard.m5digital.net.br
|
||||||
|
|
||||||
|
CONTAINERS ATIVOS:
|
||||||
|
$(docker ps --format "{{.Names}} - {{.Status}}")
|
||||||
|
|
||||||
|
PENDENTES:
|
||||||
|
$(docker ps -a --filter "status=exited" --format "{{.Names}} - {{.Status}}")
|
||||||
|
|
||||||
|
INTEGRAÇÕES:
|
||||||
|
- n8n ↔ Chatwoot: A configurar
|
||||||
|
- n8n ↔ Twenty: A configurar
|
||||||
|
- n8n ↔ Saleor: A configurar
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Relatório gerado em /tmp/relatorio_operacao.txt"
|
||||||
|
echo "🎉 OPERAÇÃO COMPLETA FINALIZADA!"
|
||||||
7
scripts/requirements.txt
Normal file
7
scripts/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
requests>=2.31.0
|
||||||
|
pandas>=2.1.0
|
||||||
|
openpyxl>=3.1.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
rich>=13.7.0
|
||||||
|
tabulate>=0.9.0
|
||||||
|
loguru>=0.7.0
|
||||||
5
scripts/resumo-diario.sh
Executable file
5
scripts/resumo-diario.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# M5 - Resumo Diário
|
||||||
|
# Data: 24/06/2026
|
||||||
|
echo "Resumo Diário - $(date)"
|
||||||
|
# Será implementado com relatórios
|
||||||
8
scripts/saleor-entrypoint.sh
Executable file
8
scripts/saleor-entrypoint.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copiar RSA key ANTES de qualquer coisa
|
||||||
|
cp /app/media/RSA_PRIVATE_KEY.pem /app/RSA_PRIVATE_KEY.pem 2>/dev/null || true
|
||||||
|
chmod 600 /app/RSA_PRIVATE_KEY.pem 2>/dev/null || true
|
||||||
|
|
||||||
|
# Executar o que foi passado como argumento
|
||||||
|
exec "$@"
|
||||||
5
scripts/saleor-start.sh
Executable file
5
scripts/saleor-start.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cp /app/media/RSA_PRIVATE_KEY.pem /tmp/RSA.pem 2>/dev/null
|
||||||
|
chmod 600 /tmp/RSA.pem 2>/dev/null
|
||||||
|
export RSA_PRIVATE_KEY=/tmp/RSA.pem
|
||||||
|
exec gunicorn --bind :8000 --workers 4 --worker-class saleor.asgi.gunicorn_worker.UvicornWorker saleor.asgi:application
|
||||||
61
scripts/telegram_user_login.py
Normal file
61
scripts/telegram_user_login.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Hermes Telegram User Account — Login Script v3
|
||||||
|
Passa phone e code_callback explicitamente via kwargs do start()
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
API_ID = 35430577
|
||||||
|
API_HASH = "bf7926bd5830a1d2ebf502e538a0d816"
|
||||||
|
SESSION_PATH = os.path.expanduser("~/.hermes/telegram_user.session")
|
||||||
|
PHONE = "+5511991975554"
|
||||||
|
CODE_FILE = "/tmp/telegram_code.txt"
|
||||||
|
|
||||||
|
def get_code():
|
||||||
|
"""Lê o código do arquivo, esperando até 120s se não existir."""
|
||||||
|
print(f"Aguardando código em {CODE_FILE} ...", flush=True)
|
||||||
|
for i in range(120):
|
||||||
|
if os.path.exists(CODE_FILE):
|
||||||
|
with open(CODE_FILE) as f:
|
||||||
|
code = f.read().strip()
|
||||||
|
if code:
|
||||||
|
os.remove(CODE_FILE)
|
||||||
|
print(f"Código recebido: {code}", flush=True)
|
||||||
|
return code
|
||||||
|
time.sleep(1)
|
||||||
|
raise TimeoutError("Tempo esgotado aguardando código")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("=" * 50)
|
||||||
|
print("Hermes Telegram — Login com Conta de Usuário")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"Sessão: {SESSION_PATH}")
|
||||||
|
print(f"Telefone: {PHONE}", flush=True)
|
||||||
|
print()
|
||||||
|
|
||||||
|
client = TelegramClient(SESSION_PATH, API_ID, API_HASH)
|
||||||
|
|
||||||
|
# Inicia com phone e code_callback via kwargs
|
||||||
|
await client.start(
|
||||||
|
phone=lambda: PHONE,
|
||||||
|
code_callback=get_code
|
||||||
|
)
|
||||||
|
|
||||||
|
me = await client.get_me()
|
||||||
|
print()
|
||||||
|
print("=" * 50)
|
||||||
|
print("✅ LOGIN REALIZADO COM SUCESSO!")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"ID: {me.id}")
|
||||||
|
print(f"Nome: {me.first_name} {me.last_name or ''}")
|
||||||
|
print(f"Username: @{me.username}")
|
||||||
|
print(f"Telefone: {me.phone}")
|
||||||
|
print()
|
||||||
|
print("Sessão salva em: ~/.hermes/telegram_user.session", flush=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
94
scripts/telegram_user_monitor.py
Normal file
94
scripts/telegram_user_monitor.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Monitora a conta de usuário do Telegram via Telethon.
|
||||||
|
Reenvia mensagens recebidas para o Hermes processar.
|
||||||
|
Mantém diálogo contínuo em /root/.hermes/telegram_user.session
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
API_ID = 35430577
|
||||||
|
API_HASH = "bf7926bd5830a1d2ebf502e538a0d816"
|
||||||
|
SESSION_FILE = "/root/.hermes/telegram_user.session"
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler("/root/.hermes/logs/telegram_user.log"),
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
log = logging.getLogger("telegram_user")
|
||||||
|
|
||||||
|
# Pasta para salvar state
|
||||||
|
STATE_FILE = "/root/.hermes/telegram_user_state.json"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
client = TelegramClient(SESSION_FILE, API_ID, API_HASH)
|
||||||
|
|
||||||
|
async with client:
|
||||||
|
me = await client.get_me()
|
||||||
|
log.info(f"=== Monitorando conta: {me.first_name} (@{me.username}) ===")
|
||||||
|
log.info(f"User ID: {me.id}")
|
||||||
|
log.info("Aguardando mensagens...")
|
||||||
|
|
||||||
|
from telethon import events
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(incoming=True))
|
||||||
|
async def handler(event):
|
||||||
|
chat = await event.get_chat()
|
||||||
|
sender = await event.get_sender()
|
||||||
|
|
||||||
|
# Ignora bots (inclusive o @Hermes_M5 / @m5onBot)
|
||||||
|
if getattr(sender, "bot", False):
|
||||||
|
log.info(f"Ignorando mensagem de bot: {getattr(sender, 'username', 'sem username')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignora mensagens enviadas por nós mesmos
|
||||||
|
if event.out:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Texto da mensagem
|
||||||
|
if hasattr(event, "message") and event.message:
|
||||||
|
text = event.message.text or event.message.message or ""
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
text = str(event)[:100]
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%d/%m %H:%M")
|
||||||
|
chat_name = getattr(chat, "title", None) or getattr(chat, "first_name", "Desconhecido")
|
||||||
|
sender_name = f"{getattr(sender, 'first_name', '')} {getattr(sender, 'last_name', '')}".strip()
|
||||||
|
username = f"(@{sender.username})" if getattr(sender, "username", None) else ""
|
||||||
|
|
||||||
|
log.info("")
|
||||||
|
log.info(f"╔══════════════════════════════════════════════════════╗")
|
||||||
|
log.info(f"║ {timestamp} | {chat_name} ║")
|
||||||
|
log.info(f"║ De: {sender_name} {username} ║")
|
||||||
|
log.info(f"╠══════════════════════════════════════════════════════╣")
|
||||||
|
# Quebra texto em linhas de até 58 chars
|
||||||
|
for i in range(0, len(text), 58):
|
||||||
|
line = text[i : i + 58]
|
||||||
|
log.info(f"║ {line:<58} ║")
|
||||||
|
log.info(f"╚══════════════════════════════════════════════════════╝")
|
||||||
|
log.info("")
|
||||||
|
|
||||||
|
# SEM auto-resposta — só loga as mensagens
|
||||||
|
# (auto-resposta desativada em 21/jun/2026)
|
||||||
|
|
||||||
|
await client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Monitoramento interrompido.")
|
||||||
7
scripts/traefik-conf/dynamic-config.yml
Normal file
7
scripts/traefik-conf/dynamic-config.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
secure-headers:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
referrerPolicy: strict-origin-when-cross-origin
|
||||||
28
scripts/traefik-conf/traefik.yml
Normal file
28
scripts/traefik-conf/traefik.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
insecure: true
|
||||||
|
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
|
||||||
|
providers:
|
||||||
|
file:
|
||||||
|
filename: /etc/traefik/routes.yml
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: irisflorencio@yahoo.com.br
|
||||||
|
storage: /letsencrypt/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
accessLog:
|
||||||
|
format: json
|
||||||
Loading…
Reference in New Issue
Block a user