112 lines
4.1 KiB
Python
112 lines
4.1 KiB
Python
import os
|
|
import logging
|
|
import requests
|
|
from repositories.monitoring_repository import monitoring_repository
|
|
from repositories.alert_repository import alert_repository
|
|
from repositories.tenant_repository import tenant_repository
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
GOTIFY_URL = os.getenv('GOTIFY_URL')
|
|
GOTIFY_APP_TOKEN = os.getenv('GOTIFY_APP_TOKEN')
|
|
|
|
|
|
def send_gotify(title, message, priority=5):
|
|
"""
|
|
Sends a push notification via Gotify to the owner's phone. Logs the
|
|
alert attempt to monitoring_events regardless of delivery success.
|
|
Never shown to clients — owner monitoring only, per CLAUDE.md §3/§27.
|
|
|
|
Data flow:
|
|
title + message + priority →
|
|
HTTP POST {GOTIFY_URL}/message →
|
|
monitoring_events record created (alert_sent reflects HTTP outcome)
|
|
"""
|
|
delivered = False
|
|
try:
|
|
response = requests.post(
|
|
f'{GOTIFY_URL}/message',
|
|
headers={'X-Gotify-Key': GOTIFY_APP_TOKEN},
|
|
json={'title': title, 'message': message, 'priority': priority},
|
|
timeout=5
|
|
)
|
|
delivered = response.status_code < 300
|
|
except Exception as e:
|
|
# Side-effect failure — must never propagate into the caller's
|
|
# primary operation (a job completing, a threshold check, etc.).
|
|
logger.error(f'Gotify alert failed: {str(e)}')
|
|
|
|
monitoring_repository.log_event(
|
|
event_type='api_health' if priority >= 10 else 'failure_rate',
|
|
severity='critical' if priority >= 10 else ('warning' if priority >= 7 else 'advisory'),
|
|
message=f'{title}: {message}',
|
|
alert_sent=delivered,
|
|
)
|
|
|
|
|
|
def create_alert(tenant_id, competitor_id, alert_type, message, product_id=None,
|
|
change_amount=None, change_percent=None):
|
|
"""
|
|
Creates a tenant-facing alert record — always visible on the
|
|
dashboard activity feed immediately (delivered_dashboard is always
|
|
true). Email/Slack/Teams/WhatsApp delivery flags are set later by
|
|
the digest workflows that actually deliver through those channels.
|
|
|
|
Data flow:
|
|
alert fields → alerts table record created with
|
|
delivered_dashboard=true, all other delivered_* flags false →
|
|
created alert record returned
|
|
"""
|
|
return alert_repository.create({
|
|
'tenant_id': tenant_id,
|
|
'competitor_id': competitor_id,
|
|
'product_id': product_id,
|
|
'alert_type': alert_type,
|
|
'message': message,
|
|
'change_amount': change_amount,
|
|
'change_percent': change_percent,
|
|
'delivered_dashboard': True,
|
|
'delivered_email': False,
|
|
'delivered_slack': False,
|
|
'delivered_teams': False,
|
|
'delivered_whatsapp': False,
|
|
'read': False,
|
|
})
|
|
|
|
|
|
def send_daily_digest(tenant_id):
|
|
"""
|
|
Sends the 6pm daily digest for one tenant — every alert not yet
|
|
included in a digest, in a single email, per CLAUDE.md §15. Called
|
|
once per active tenant by the n8n daily-digest workflow (system-
|
|
level iteration, not a PM-facing route — see api.py's
|
|
/internal/daily-digest/<tenant_id>).
|
|
|
|
Data flow:
|
|
tenant_id → tenant record (email/name) →
|
|
alert_repository.list_undelivered_email() →
|
|
no alerts: skip entirely, no empty email sent →
|
|
alerts found: email_service.send_daily_digest_email() →
|
|
success: every included alert's delivered_email flipped true
|
|
"""
|
|
tenant = tenant_repository.get_by_id(tenant_id)
|
|
if not tenant or not tenant.get('email'):
|
|
logger.error(f'No tenant/email on file for {tenant_id} — skipping daily digest')
|
|
return False
|
|
|
|
alerts = alert_repository.list_undelivered_email(tenant_id)
|
|
if not alerts:
|
|
return False
|
|
|
|
# Imported lazily — same reasoning as brief_service/email_service's
|
|
# own lazy `import resend`: keeps this module importable without
|
|
# every dependency configured in test environments.
|
|
from services import email_service
|
|
sent = email_service.send_daily_digest_email(tenant['email'], tenant.get('name'), alerts)
|
|
|
|
if sent:
|
|
for alert in alerts:
|
|
alert_repository.mark_email_delivered(alert['id'])
|
|
|
|
return sent
|