Add supplements, kettlebell, calendar, push notifications, and PWA support

- Supplement tracking: CRUD endpoints, /today, /logs, Supplements page
- Kettlebell workouts: session tracking, analytics endpoint, ActiveSession page
- Calendar module: events CRUD, calendar components
- Push notifications: VAPID keys, PushSubscription model, APScheduler reminders,
  service worker with push/notificationclick handlers, Profile notifications UI
- PWA: vite-plugin-pwa, manifest, icons, service worker generation
- Frontend: TypeScript types, API modules, ConfirmModal, toast notifications
- Auth fixes: password hashing, nutrition endpoint auth
- CLAUDE.md: project documentation and development guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-03-20 18:57:03 -06:00
parent bd91eb4171
commit f279907ae3
61 changed files with 9256 additions and 85 deletions

47
backend/app/scheduler.py Normal file
View File

@@ -0,0 +1,47 @@
from datetime import datetime, timezone
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from sqlmodel import Session, select
from app.db import engine
from app.models.push_subscription import PushSubscription
scheduler = BackgroundScheduler(timezone="UTC")
def send_supplement_reminders() -> None:
from app.api.v1.endpoints.push import send_push
now_utc = datetime.now(timezone.utc)
with Session(engine) as session:
subs = session.exec(
select(PushSubscription).where(PushSubscription.is_active == True) # noqa: E712
).all()
due = []
for sub in subs:
try:
tz = pytz.timezone(sub.timezone)
local_now = now_utc.astimezone(tz)
if local_now.hour == sub.reminder_hour and local_now.minute == sub.reminder_minute:
due.append(sub)
except Exception:
pass
if due:
send_push(due, title="Supplement Reminder", body="Time to log your supplements for today!", session=session)
session.commit()
def start_scheduler() -> None:
scheduler.add_job(
send_supplement_reminders,
CronTrigger(minute="*"),
id="supplement_reminders",
replace_existing=True,
)
scheduler.start()
def stop_scheduler() -> None:
scheduler.shutdown(wait=False)