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:
109
api/routes/tasks.py
Normal file
109
api/routes/tasks.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user