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.*
|
||||
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