Add one-off tasks/appointments feature
- DB: tasks table with scheduled_datetime, reminder_minutes_before, advance_notified, status - API: CRUD routes GET/POST /api/tasks, PATCH/DELETE /api/tasks/<id> - Scheduler: check_task_reminders() fires advance + at-time notifications, tracks advance_notified to prevent double-fire - Bot: handle_task() with add/list/done/cancel/delete actions + datetime resolution helper - AI: task interaction type + examples added to command_parser - Web: task list page with overdue/notified color coding + new task form with datetime-local picker - Nav: replaced Templates with Tasks in bottom nav Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -515,6 +515,69 @@ def _check_per_user_midnight_schedules():
|
||||
)
|
||||
|
||||
|
||||
def check_task_reminders():
|
||||
"""Check one-off tasks for advance and at-time reminders."""
|
||||
from datetime import timedelta
|
||||
try:
|
||||
tasks = postgres.select("tasks", where={"status": "pending"})
|
||||
if not tasks:
|
||||
return
|
||||
|
||||
user_tasks = {}
|
||||
for task in tasks:
|
||||
uid = task.get("user_uuid")
|
||||
user_tasks.setdefault(uid, []).append(task)
|
||||
|
||||
for user_uuid, task_list in user_tasks.items():
|
||||
now = _user_now_for(user_uuid)
|
||||
current_hhmm = now.strftime("%H:%M")
|
||||
current_date = now.date()
|
||||
user_settings = None # lazy-load once per user
|
||||
|
||||
for task in task_list:
|
||||
raw_dt = task.get("scheduled_datetime")
|
||||
if not raw_dt:
|
||||
continue
|
||||
sched_dt = (
|
||||
raw_dt
|
||||
if isinstance(raw_dt, datetime)
|
||||
else datetime.fromisoformat(str(raw_dt))
|
||||
)
|
||||
sched_date = sched_dt.date()
|
||||
sched_hhmm = sched_dt.strftime("%H:%M")
|
||||
reminder_min = task.get("reminder_minutes_before") or 0
|
||||
|
||||
# Advance reminder
|
||||
if reminder_min > 0 and not task.get("advance_notified"):
|
||||
adv_dt = sched_dt - timedelta(minutes=reminder_min)
|
||||
if adv_dt.date() == current_date and adv_dt.strftime("%H:%M") == current_hhmm:
|
||||
if user_settings is None:
|
||||
user_settings = notifications.getNotificationSettings(user_uuid)
|
||||
if user_settings:
|
||||
msg = f"⏰ In {reminder_min} min: {task['title']}"
|
||||
if task.get("description"):
|
||||
msg += f" — {task['description']}"
|
||||
notifications._sendToEnabledChannels(user_settings, msg, user_uuid=user_uuid)
|
||||
postgres.update("tasks", {"advance_notified": True}, {"id": task["id"]})
|
||||
|
||||
# At-time reminder
|
||||
if sched_date == current_date and sched_hhmm == current_hhmm:
|
||||
if user_settings is None:
|
||||
user_settings = notifications.getNotificationSettings(user_uuid)
|
||||
if user_settings:
|
||||
msg = f"📋 Now: {task['title']}"
|
||||
if task.get("description"):
|
||||
msg += f"\n{task['description']}"
|
||||
notifications._sendToEnabledChannels(user_settings, msg, user_uuid=user_uuid)
|
||||
postgres.update(
|
||||
"tasks",
|
||||
{"status": "notified", "updated_at": datetime.utcnow().isoformat()},
|
||||
{"id": task["id"]},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking task reminders: {e}")
|
||||
|
||||
|
||||
def poll_callback():
|
||||
"""Called every POLL_INTERVAL seconds."""
|
||||
# Create daily schedules per-user at their local midnight
|
||||
@@ -537,6 +600,7 @@ def poll_callback():
|
||||
# Original checks
|
||||
check_routine_reminders()
|
||||
check_refills()
|
||||
check_task_reminders()
|
||||
|
||||
|
||||
def daemon_loop():
|
||||
|
||||
Reference in New Issue
Block a user