- 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>
110 lines
4.1 KiB
Python
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
|