├── .env.example ├── requirements.txt ├── vercel.json ├── .gitignore ├── teste_email.py ├── database.py ├── TROUBLESHOOTING.md ├── teste-integracao.html ├── app.py └── README.md /.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biancaalvess/Notifica-Site/HEAD/.env.example -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-cors 3 | python-dotenv 4 | gunicorn==21.2.0 5 | user-agents 6 | pytz -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "app.py", 6 | "use": "@vercel/python" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "app.py" 13 | } 14 | ], 15 | "crons": [ 16 | { 17 | "path": "/enviar-relatorio-diario", 18 | "schedule": "0 20 * * *" 19 | } 20 | ] 21 | } 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | !.env.example 4 | 5 | # Python 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | *.so 10 | .Python 11 | env/ 12 | venv/ 13 | ENV/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # IDE 31 | .vscode/ 32 | .idea/ 33 | *.swp 34 | *.swo 35 | *~ 36 | 37 | # OS 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # Logs 42 | *.log 43 | 44 | # Arquivos de persistência (dados da API) 45 | visitas.json 46 | ultimo_email.json 47 | 48 | -------------------------------------------------------------------------------- /teste_email.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | import smtplib 3 | from email.mime.text import MIMEText 4 | from datetime import datetime 5 | import os 6 | 7 | # Carrega variáveis de ambiente 8 | load_dotenv() 9 | 10 | EMAIL_ADDRESS = os.getenv('EMAIL_ADDRESS') 11 | EMAIL_PASSWORD = os.getenv('EMAIL_PASSWORD') 12 | SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.gmail.com') 13 | SMTP_PORT = int(os.getenv('SMTP_PORT', 587)) 14 | 15 | print("Testando envio de e-mail...") 16 | print(f"De: {EMAIL_ADDRESS}") 17 | print(f"Para: {EMAIL_ADDRESS}") 18 | print(f"SMTP: {SMTP_SERVER}:{SMTP_PORT}") 19 | 20 | conteudo_html = f""" 21 | 22 |
23 |Este e um email de teste da API de notificacoes.
25 |Data e Hora: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}
26 |Se voce recebeu este email, esta funcionando!
27 | 28 | 29 | """ 30 | 31 | msg = MIMEText(conteudo_html, 'html') 32 | msg['Subject'] = 'Teste de Email - API Notificacao' 33 | msg['From'] = EMAIL_ADDRESS 34 | msg['To'] = EMAIL_ADDRESS 35 | 36 | try: 37 | print("\nTentando conectar ao servidor SMTP...") 38 | with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as servidor: 39 | print("Iniciando TLS...") 40 | servidor.starttls() 41 | print("Fazendo login...") 42 | servidor.login(EMAIL_ADDRESS, EMAIL_PASSWORD) 43 | print("Enviando email...") 44 | servidor.send_message(msg) 45 | print("\nSUCESSO! Email enviado com sucesso!") 46 | print("Verifique sua caixa de entrada.") 47 | except smtplib.SMTPAuthenticationError as e: 48 | print(f"\nERRO: Falha na autenticacao: {e}") 49 | print("\nPossiveis causas:") 50 | print("1. Email ou senha incorretos") 51 | print("2. Senha de aplicativo nao configurada") 52 | print("3. Verificacao de duas etapas desabilitada") 53 | except Exception as e: 54 | print(f"\nERRO: {e}") 55 | print("\nPossiveis causas:") 56 | print("1. Problemas de conexao") 57 | print("2. Credenciais incorretas") 58 | print("3. Firewall bloqueando a conexao") 59 | 60 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from datetime import datetime 4 | import threading 5 | 6 | # Configuração dos arquivos 7 | # O uso de /tmp é necessário para ambientes serverless como Vercel (temporário) 8 | VISITAS_FILE = '/tmp/visitas.json' if os.path.exists('/tmp') else 'visitas.json' 9 | ULTIMO_EMAIL_FILE = '/tmp/ultimo_email.json' if os.path.exists('/tmp') else 'ultimo_email.json' 10 | 11 | # Lock para evitar erros de escrita simultânea 12 | bloqueio = threading.Lock() 13 | 14 | def carregar_visitas(): 15 | """Carrega todas as visitas salvas no arquivo JSON.""" 16 | try: 17 | if os.path.exists(VISITAS_FILE): 18 | with open(VISITAS_FILE, 'r', encoding='utf-8') as f: 19 | dados = json.load(f) 20 | visitas = [] 21 | for v in dados: 22 | try: 23 | # Converte string ISO de volta para datetime 24 | if isinstance(v['tempo'], str): 25 | v['tempo'] = datetime.fromisoformat(v['tempo'].replace('Z', '+00:00')) 26 | visitas.append(v) 27 | except: 28 | continue 29 | return visitas 30 | except Exception as e: 31 | print(f"Erro ao carregar visitas: {e}") 32 | return [] 33 | 34 | def salvar_visita(visita): 35 | """Adiciona uma única visita e salva no arquivo.""" 36 | with bloqueio: 37 | visitas = carregar_visitas() 38 | 39 | # Garante que o tempo esteja em formato serializável (ISO string) 40 | visita_copy = visita.copy() 41 | if isinstance(visita_copy['tempo'], datetime): 42 | visita_copy['tempo'] = visita_copy['tempo'].isoformat() 43 | 44 | # Adiciona à lista existente 45 | visitas_salvar = [] 46 | 47 | # Prepara a lista atual para salvar (converte datetimes para string) 48 | for v in visitas: 49 | v_save = v.copy() 50 | if isinstance(v_save['tempo'], datetime): 51 | v_save['tempo'] = v_save['tempo'].isoformat() 52 | visitas_salvar.append(v_save) 53 | 54 | visitas_salvar.append(visita_copy) 55 | 56 | _escrever_arquivo(visitas_salvar) 57 | 58 | def _escrever_arquivo(dados): 59 | """Função interna para escrever no JSON.""" 60 | try: 61 | with open(VISITAS_FILE, 'w', encoding='utf-8') as f: 62 | json.dump(dados, f, ensure_ascii=False, indent=2) 63 | except Exception as e: 64 | print(f"Erro ao salvar arquivo: {e}") 65 | 66 | def obter_ultimo_email_enviado(): 67 | """Recupera a data do último envio de e-mail.""" 68 | try: 69 | if os.path.exists(ULTIMO_EMAIL_FILE): 70 | with open(ULTIMO_EMAIL_FILE, 'r', encoding='utf-8') as f: 71 | dados = json.load(f) 72 | if dados and 'timestamp' in dados: 73 | return datetime.fromisoformat(dados['timestamp'].replace('Z', '+00:00')) 74 | except Exception as e: 75 | print(f"Erro ao carregar último email: {e}") 76 | return None 77 | 78 | def salvar_ultimo_email_enviado(timestamp): 79 | """Salva a data do envio de e-mail atual.""" 80 | try: 81 | dados = {'timestamp': timestamp.isoformat()} 82 | with open(ULTIMO_EMAIL_FILE, 'w', encoding='utf-8') as f: 83 | json.dump(dados, f) 84 | except Exception as e: 85 | print(f"Erro ao salvar último email: {e}") -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # 🔧 Troubleshooting - API não está funcionando 2 | 3 | ## ✅ Arquitetura Atualizada para Vercel (Serverless) 4 | 5 | A API foi refatorada para funcionar corretamente no ambiente serverless do Vercel: 6 | 7 | - **Persistência**: Dados salvos em arquivos JSON (funciona no Vercel) 8 | - **Cron Jobs**: Relatório diário via Vercel Cron (configurado em `vercel.json`) 9 | - **Anti-duplicatas**: Usa persistência em arquivo (funciona com múltiplas instâncias) 10 | 11 | ### Relatório Diário 12 | 13 | O relatório diário agora é enviado via **Cron Job do Vercel** às 17:00 (horário de Brasília). 14 | 15 | - Rota: `/enviar-relatorio-diario` 16 | - Configurado em: `vercel.json` → `crons` 17 | - Horário: 20:00 UTC (17:00 Brasília) 18 | 19 | ## Problemas Comuns e Soluções 20 | 21 | ### 1. API não responde no Vercel 22 | 23 | **Sintomas**: Erro 404 ou timeout ao acessar a API 24 | 25 | **Soluções**: 26 | - Verifique se o arquivo `vercel.json` existe e está configurado 27 | - Certifique-se que as rotas estão mapeadas corretamente 28 | - Verifique os logs no painel do Vercel 29 | 30 | ### 2. E-mails não estão sendo enviados 31 | 32 | **Sintomas**: API responde mas não recebe e-mails 33 | 34 | **Verificações**: 35 | 1. **Variáveis de ambiente no Vercel**: 36 | - Vá em Settings → Environment Variables 37 | - Verifique se estão configuradas: 38 | - `EMAIL_ADDRESS` 39 | - `EMAIL_PASSWORD` 40 | - `SMTP_SERVER` 41 | - `SMTP_PORT` 42 | 43 | 2. **Verificar logs do Vercel**: 44 | - Acesse o painel do Vercel 45 | - Vá em Deployments → [seu deployment] → Logs 46 | - Procure por mensagens como: 47 | - `[SUCESSO] NOTIFICACAO ENVIADA` 48 | - `[ERRO] AUTENTICACAO FALHOU` 49 | - `DEBUG: Função enviar_notificacao_imediata chamada` 50 | 51 | 3. **Testar SMTP**: 52 | ```bash 53 | python teste_email.py 54 | ``` 55 | 56 | ### 3. Erro "Module not found" 57 | 58 | **Sintomas**: Erro ao fazer deploy no Vercel 59 | 60 | **Solução**: 61 | - Verifique se `requirements.txt` está completo 62 | - Certifique-se que todas as dependências estão listadas 63 | 64 | ### 4. Threads não funcionam no Vercel 65 | 66 | **Problema**: O Vercel é serverless e pode ter limitações com threads 67 | 68 | **Solução**: 69 | - O código já está adaptado para funcionar sem threads persistentes 70 | - As threads são criadas apenas para envio de email (daemon threads) 71 | - O schedule pode não funcionar bem no Vercel (funcionalidade serverless) 72 | 73 | ### 5. Timeout no Vercel 74 | 75 | **Sintomas**: Requisições demoram muito ou dão timeout 76 | 77 | **Soluções**: 78 | - Aumente o timeout no `vercel.json` (se necessário) 79 | - Verifique se o SMTP não está demorando muito 80 | - Considere usar async/await para operações de rede 81 | 82 | ### 6. Variáveis de ambiente não estão sendo carregadas 83 | 84 | **Sintomas**: API não encontra EMAIL_ADDRESS ou EMAIL_PASSWORD 85 | 86 | **Solução**: 87 | 1. No Vercel: 88 | - Settings → Environment Variables 89 | - Adicione todas as variáveis 90 | - Certifique-se de fazer redeploy após adicionar 91 | 92 | 2. Verifique no código: 93 | ```python 94 | print(f"Email configurado: {EMAIL_ADDRESS}") 95 | ``` 96 | 97 | ## Teste Local vs Vercel 98 | 99 | ### Teste Local 100 | ```bash 101 | python app.py 102 | # Acesse: http://localhost:5000/track-visit 103 | ``` 104 | 105 | ### Teste Vercel 106 | ```bash 107 | # Testar tracking 108 | curl -X POST https://sua-api.vercel.app/track-visit 109 | 110 | # Testar health check 111 | curl https://sua-api.vercel.app/health 112 | 113 | # Testar relatório diário manualmente 114 | curl https://sua-api.vercel.app/enviar-relatorio-diario 115 | ``` 116 | 117 | ## Verificar Logs 118 | 119 | ### Logs Locais 120 | - Execute `python app.py` e veja o console 121 | 122 | ### Logs Vercel 123 | 1. Acesse: https://vercel.com/dashboard 124 | 2. Selecione seu projeto 125 | 3. Vá em Deployments 126 | 4. Clique no deployment mais recente 127 | 5. Veja a aba "Logs" 128 | 129 | ## Checklist de Debugging 130 | 131 | - [ ] API responde com 200 OK? 132 | - [ ] Variáveis de ambiente configuradas no Vercel? 133 | - [ ] Logs mostram "Email configurado: ..."? 134 | - [ ] Logs mostram "DEBUG: Função enviar_notificacao_imediata chamada"? 135 | - [ ] Logs mostram "[SUCESSO]" ou "[ERRO]"? 136 | - [ ] Teste local funciona? 137 | - [ ] `vercel.json` existe e está correto (incluindo cron)? 138 | - [ ] `requirements.txt` está completo (sem schedule)? 139 | - [ ] Rota `/health` retorna status ok? 140 | - [ ] Rota `/enviar-relatorio-diario` funciona manualmente? 141 | - [ ] Cron job está configurado no Vercel? 142 | 143 | ## Próximos Passos 144 | 145 | 1. Verifique os logs do Vercel 146 | 2. Teste a API localmente 147 | 3. Compare o comportamento local vs Vercel 148 | 4. Verifique variáveis de ambiente 149 | 5. Teste o envio de email isoladamente 150 | 151 | ## Suporte 152 | 153 | Se o problema persistir: 154 | 1. Verifique os logs completos do Vercel 155 | 2. Teste localmente para isolar o problema 156 | 3. Compare configurações entre local e Vercel 157 | 158 | -------------------------------------------------------------------------------- /teste-integracao.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |Esta página simula uma visita ao site devbianca.tech
73 |A API foi chamada automaticamente!
74 | 75 |Carregando...
77 |Vamos torcer por uma entrevista, boa sorte!! 🎉
58 |
59 | Data e Hora: {agora.strftime('%d/%m/%Y %H:%M:%S')} (Horário de Brasília)
60 | 61 | 62 | """ 63 | 64 | msg = MIMEText(conteudo_html, 'html') 65 | msg['Subject'] = f'Nova Visita em devbianca.tech - {agora.strftime("%H:%M")}' 66 | msg['From'] = EMAIL_ADDRESS 67 | msg['To'] = EMAIL_ADDRESS 68 | 69 | try: 70 | with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as servidor: 71 | servidor.starttls() 72 | servidor.login(EMAIL_ADDRESS, EMAIL_PASSWORD) 73 | servidor.send_message(msg) 74 | print(f"[SUCESSO] Notificação enviada em {agora.strftime('%d/%m/%Y %H:%M:%S')}") 75 | database.salvar_ultimo_email_enviado(agora) 76 | return True 77 | except Exception as e: 78 | print(f"[ERRO] Falha ao enviar email: {e}") 79 | return False 80 | 81 | def enviar_relatorio_diario(): 82 | if not EMAIL_ADDRESS or not EMAIL_PASSWORD: 83 | return {"error": "Email não configurado"}, 500 84 | 85 | visitas = database.carregar_visitas() 86 | 87 | if not visitas: 88 | return {"message": "Nenhuma visita registrada no total", "total": 0}, 200 89 | 90 | agora = agora_brasilia() 91 | 92 | visitas_hoje = [] 93 | for visita in visitas: 94 | try: 95 | tempo_visita = visita['tempo'] 96 | if isinstance(tempo_visita, str): 97 | tempo_visita = datetime.fromisoformat(tempo_visita.replace('Z', '+00:00')) 98 | 99 | if tempo_visita.date() == agora.date(): 100 | visitas_hoje.append(visita) 101 | except: 102 | continue 103 | 104 | total_geral = len(visitas) 105 | total_hoje = len(visitas_hoje) 106 | 107 | detalhes_visitas_hoje = "" 108 | if visitas_hoje: 109 | for visita in visitas_hoje: 110 | tempo = visita['tempo'] 111 | if isinstance(tempo, str): 112 | tempo = datetime.fromisoformat(tempo.replace('Z', '+00:00')) 113 | detalhes_visitas_hoje += f"Data: {agora.strftime('%d/%m/%Y')}
122 |Vamos torcer por uma entrevista!!
129 | 130 |