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:
2026-02-19 16:43:42 -06:00
parent 2951382c51
commit bebc609091
11 changed files with 828 additions and 4 deletions

View File

@@ -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():