06/05 Optimize app: Add Plaid web hook, healthcheck

This commit is contained in:
2026-06-05 13:57:36 -04:00
parent 452374365c
commit 2f62417c9f
9 changed files with 449 additions and 44 deletions
+87
View File
@@ -0,0 +1,87 @@
"""
Health check endpoint — no authentication required.
Used by uptime monitors (UptimeRobot, etc.) to verify the app and subsystems.
"""
import logging
import time
from datetime import datetime
from flask import Blueprint, jsonify
from sqlalchemy import func, text
from app.extensions import db
health_bp = Blueprint('health', __name__)
log = logging.getLogger('app.health')
@health_bp.route('/health')
def health_check():
db_result = _check_db()
cron_result = _check_crons()
status = 'ok'
if db_result['status'] != 'ok':
status = 'degraded'
if any(v.get('overdue') for v in cron_result.values() if isinstance(v, dict)):
status = 'degraded'
return jsonify({
'status': status,
'timestamp': datetime.utcnow().isoformat() + 'Z',
'db': db_result,
'crons': cron_result,
}), 200 if status == 'ok' else 503
def _check_db():
t0 = time.monotonic()
try:
db.session.execute(text('SELECT 1'))
ms = round((time.monotonic() - t0) * 1000, 1)
return {'status': 'ok', 'latency_ms': ms}
except Exception as e:
log.error('[health] DB check failed: %s', e)
return {'status': 'error', 'error': str(e)}
def _check_crons():
from app.models.fx_rate import FxRate
from app.models.ai_insight import AiInsight
from app.models.net_worth_snapshot import NetWorthSnapshot
from app.models.investment import Investment
return {
# expected daily — overdue after 48 h
'fetch_fx_rate': _stat(
db.session.query(func.max(FxRate.fetched_at)).scalar(),
max_hours=48,
),
# expected daily — overdue after 48 h
'daily_ai_insight': _stat(
db.session.query(func.max(AiInsight.created_at))
.filter(AiInsight.insight_type == 'daily_summary').scalar(),
max_hours=48,
),
# expected monthly — overdue after 35 days
'daily_snapshot': _stat(
db.session.query(func.max(NetWorthSnapshot.created_at)).scalar(),
max_hours=35 * 24,
),
# expected weekdays — overdue after 4 days
'fetch_prices': _stat(
db.session.query(func.max(Investment.last_price_update)).scalar(),
max_hours=4 * 24,
),
}
def _stat(last_run_dt, max_hours):
if last_run_dt is None:
return {'last_run': None, 'age_hours': None, 'overdue': False}
age = round((datetime.utcnow() - last_run_dt).total_seconds() / 3600, 1)
return {
'last_run': last_run_dt.isoformat() + 'Z',
'age_hours': age,
'overdue': age > max_hours,
}