Files
Synculous-2/api/routes/tasks.py
chelsea bebc609091 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>
2026-02-19 16:43:42 -06:00

110 lines
4.1 KiB
Python

"""
api/routes/tasks.py - One-off scheduled task CRUD
"""
import uuid
import flask
import jwt
import os
from datetime import datetime
import core.postgres as postgres
JWT_SECRET = os.getenv("JWT_SECRET")
def _get_user_uuid(request):
"""Extract and validate user UUID from JWT token."""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header[7:]
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
return payload.get("sub")
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def register(app):
@app.route("/api/tasks", methods=["GET"])
def get_tasks():
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
status_filter = flask.request.args.get("status", "pending")
if status_filter == "all":
tasks = postgres.select(
"tasks",
where={"user_uuid": user_uuid},
order_by="scheduled_datetime ASC",
)
else:
tasks = postgres.select(
"tasks",
where={"user_uuid": user_uuid, "status": status_filter},
order_by="scheduled_datetime ASC",
)
# Serialize datetimes for JSON
for t in tasks:
for key in ("scheduled_datetime", "created_at", "updated_at"):
if key in t and hasattr(t[key], "isoformat"):
t[key] = t[key].isoformat()
return flask.jsonify(tasks), 200
@app.route("/api/tasks", methods=["POST"])
def create_task():
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "missing body"}), 400
title = data.get("title", "").strip()
scheduled_datetime = data.get("scheduled_datetime", "").strip()
if not title:
return flask.jsonify({"error": "title is required"}), 400
if not scheduled_datetime:
return flask.jsonify({"error": "scheduled_datetime is required"}), 400
task_id = str(uuid.uuid4())
task = {
"id": task_id,
"user_uuid": user_uuid,
"title": title,
"description": data.get("description") or None,
"scheduled_datetime": scheduled_datetime,
"reminder_minutes_before": int(data.get("reminder_minutes_before", 15)),
"status": "pending",
}
postgres.insert("tasks", task)
return flask.jsonify(task), 201
@app.route("/api/tasks/<task_id>", methods=["PATCH"])
def update_task(task_id):
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
task = postgres.select_one("tasks", {"id": task_id, "user_uuid": user_uuid})
if not task:
return flask.jsonify({"error": "not found"}), 404
data = flask.request.get_json() or {}
updates = {}
for field in ["title", "description", "scheduled_datetime", "reminder_minutes_before", "status"]:
if field in data:
updates[field] = data[field]
updates["updated_at"] = datetime.utcnow().isoformat()
postgres.update("tasks", updates, {"id": task_id})
return flask.jsonify({**{k: (v.isoformat() if hasattr(v, "isoformat") else v) for k, v in task.items()}, **updates}), 200
@app.route("/api/tasks/<task_id>", methods=["DELETE"])
def delete_task(task_id):
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
task = postgres.select_one("tasks", {"id": task_id, "user_uuid": user_uuid})
if not task:
return flask.jsonify({"error": "not found"}), 404
postgres.delete("tasks", {"id": task_id})
return flask.jsonify({"success": True}), 200