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:
Iris Florencio 2026-07-06 18:50:26 -03:00
parent d81347e9fc
commit 9c2f7ff7b4
30 changed files with 2184 additions and 0 deletions

28
.gitignore vendored
View File

@ -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
View 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
View 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
View 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
View 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/
```

View 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

View 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
View 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
View 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

View 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
View 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
View File

@ -0,0 +1,5 @@
#!/bin/bash
# M5 - Compliance Check
# Data: 24/06/2026
echo "Compliance Check - $(date)"
# Será implementado com validações

View 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()

View 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
View 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
View 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

View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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())

View 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.")

View File

@ -0,0 +1,7 @@
http:
middlewares:
secure-headers:
headers:
frameDeny: true
contentTypeNosniff: true
referrerPolicy: strict-origin-when-cross-origin

View 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