Refactor cron to call API via HTTP, capture base_url from browser
CI / test-and-package (push) Successful in 33s
CI / test-and-package (push) Successful in 33s
- meal-cron now runs send_reminders_cron.sh (simple wget loop, no DB mount) - New /api/cron-tick endpoint handles all timezone logic server-side - base_url captured from request.host_url when user saves ntfy settings, stored per-user — no APP_BASE_URL env var needed - Cron endpoint protected by CRON_SECRET shared env var - Removed send_reminders_cron.py, DB volume from cron container - tests: 3 new cron-tick + base_url capture tests (69 total)
This commit is contained in:
@@ -27,9 +27,14 @@ from models import (
|
||||
get_user_by_callback_token,
|
||||
set_household_timezone,
|
||||
get_household_by_id,
|
||||
get_all_households,
|
||||
get_users_with_ntfy,
|
||||
update_reminder_sent,
|
||||
)
|
||||
from i18n import t
|
||||
from ntfy import send_test_notification
|
||||
from ntfy import send_test_notification, send_meal_reminder
|
||||
import os
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
meals_bp = Blueprint("meals", __name__)
|
||||
|
||||
@@ -268,7 +273,10 @@ def ntfy_settings():
|
||||
callback_token = ""
|
||||
ntfy_token = ""
|
||||
|
||||
set_user_ntfy(session["user_id"], ntfy_url, ntfy_token, callback_token)
|
||||
# Capture the base URL from the user's browser request
|
||||
base_url = request.host_url.rstrip('/') if ntfy_url else ""
|
||||
|
||||
set_user_ntfy(session["user_id"], ntfy_url, ntfy_token, callback_token, base_url)
|
||||
flash(t("Ntfy settings saved."), "success")
|
||||
return redirect(url_for("meals.dashboard"))
|
||||
|
||||
@@ -332,3 +340,66 @@ def ntfy_callback():
|
||||
|
||||
upsert_response(user["id"], period["id"], status)
|
||||
return {"message": "Response recorded", "status": status}, 200
|
||||
|
||||
|
||||
# ── Cron tick (called by meal-cron container) ─────────────────────────────────
|
||||
|
||||
REMINDER_HOURS = {"lunch": 10, "dinner": 16}
|
||||
|
||||
|
||||
def _resolve_timezone(tz_str: str) -> ZoneInfo:
|
||||
try:
|
||||
return ZoneInfo(tz_str)
|
||||
except Exception:
|
||||
return ZoneInfo("UTC")
|
||||
|
||||
|
||||
@meals_bp.route("/api/cron-tick", methods=["POST"])
|
||||
def cron_tick():
|
||||
"""Called by the cron container every 60s.
|
||||
|
||||
Checks each household's local time and sends ntfy reminders
|
||||
at 10:00 (lunch) and 16:00 (dinner). Protected by X-Cron-Secret header.
|
||||
"""
|
||||
expected = os.environ.get("CRON_SECRET", "")
|
||||
if expected and request.headers.get("X-Cron-Secret") != expected:
|
||||
return {"error": "unauthorized"}, 403
|
||||
|
||||
now_utc = datetime.now(ZoneInfo("UTC"))
|
||||
sent = 0
|
||||
|
||||
for hh in get_all_households():
|
||||
tz = _resolve_timezone(hh.get("timezone", "UTC"))
|
||||
local_now = now_utc.astimezone(tz)
|
||||
today_str = local_now.strftime("%Y-%m-%d")
|
||||
hour = local_now.hour
|
||||
minute = local_now.minute
|
||||
|
||||
for meal_type, trigger_hour in REMINDER_HOURS.items():
|
||||
if hour != trigger_hour or minute >= 5:
|
||||
continue
|
||||
|
||||
last_col = f"last_{meal_type}_reminder"
|
||||
if hh.get(last_col) == today_str:
|
||||
continue
|
||||
|
||||
users = get_users_with_ntfy(hh["id"])
|
||||
if not users:
|
||||
continue
|
||||
|
||||
for user in users:
|
||||
base_url = user.get("base_url", "") or request.host_url.rstrip("/")
|
||||
ok = send_meal_reminder(
|
||||
ntfy_url=user["ntfy_url"],
|
||||
ntfy_token=user["ntfy_token"],
|
||||
callback_token=user["callback_token"],
|
||||
meal_type=meal_type,
|
||||
for_date=local_now.date(),
|
||||
app_base_url=base_url,
|
||||
)
|
||||
if ok:
|
||||
sent += 1
|
||||
|
||||
update_reminder_sent(hh["id"], meal_type, today_str)
|
||||
|
||||
return {"sent": sent}, 200
|
||||
|
||||
Reference in New Issue
Block a user