|
|
|
|
@@ -104,25 +104,29 @@ class _TaskView:
|
|
|
|
|
@router.get("/tasks", response_class=HTMLResponse)
|
|
|
|
|
async def tasks_page(request: Request):
|
|
|
|
|
"""Render the main task queue page with 3-column layout."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
pending = [
|
|
|
|
|
_TaskView(_row_to_dict(r))
|
|
|
|
|
for r in db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('pending_approval') ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
]
|
|
|
|
|
active = [
|
|
|
|
|
_TaskView(_row_to_dict(r))
|
|
|
|
|
for r in db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
]
|
|
|
|
|
completed = [
|
|
|
|
|
_TaskView(_row_to_dict(r))
|
|
|
|
|
for r in db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
|
|
|
|
|
).fetchall()
|
|
|
|
|
]
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
pending = [
|
|
|
|
|
_TaskView(_row_to_dict(r))
|
|
|
|
|
for r in db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('pending_approval') ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
]
|
|
|
|
|
active = [
|
|
|
|
|
_TaskView(_row_to_dict(r))
|
|
|
|
|
for r in db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
]
|
|
|
|
|
completed = [
|
|
|
|
|
_TaskView(_row_to_dict(r))
|
|
|
|
|
for r in db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
|
|
|
|
|
).fetchall()
|
|
|
|
|
]
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
pending, active, completed = [], [], []
|
|
|
|
|
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request,
|
|
|
|
|
@@ -146,10 +150,14 @@ async def tasks_page(request: Request):
|
|
|
|
|
@router.get("/tasks/pending", response_class=HTMLResponse)
|
|
|
|
|
async def tasks_pending(request: Request):
|
|
|
|
|
"""Return HTMX partial for pending approval tasks."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status='pending_approval' ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status='pending_approval' ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
return HTMLResponse('<div class="empty-column">Database unavailable</div>')
|
|
|
|
|
tasks = [_TaskView(_row_to_dict(r)) for r in rows]
|
|
|
|
|
parts = []
|
|
|
|
|
for task in tasks:
|
|
|
|
|
@@ -166,10 +174,14 @@ async def tasks_pending(request: Request):
|
|
|
|
|
@router.get("/tasks/active", response_class=HTMLResponse)
|
|
|
|
|
async def tasks_active(request: Request):
|
|
|
|
|
"""Return HTMX partial for active (approved/running/paused) tasks."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
return HTMLResponse('<div class="empty-column">Database unavailable</div>')
|
|
|
|
|
tasks = [_TaskView(_row_to_dict(r)) for r in rows]
|
|
|
|
|
parts = []
|
|
|
|
|
for task in tasks:
|
|
|
|
|
@@ -186,10 +198,14 @@ async def tasks_active(request: Request):
|
|
|
|
|
@router.get("/tasks/completed", response_class=HTMLResponse)
|
|
|
|
|
async def tasks_completed(request: Request):
|
|
|
|
|
"""Return HTMX partial for completed/vetoed/failed tasks (last 50)."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
|
|
|
|
|
).fetchall()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
|
|
|
|
|
).fetchall()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
return HTMLResponse('<div class="empty-column">Database unavailable</div>')
|
|
|
|
|
tasks = [_TaskView(_row_to_dict(r)) for r in rows]
|
|
|
|
|
parts = []
|
|
|
|
|
for task in tasks:
|
|
|
|
|
@@ -225,13 +241,17 @@ async def create_task_form(
|
|
|
|
|
now = datetime.now(UTC).isoformat()
|
|
|
|
|
priority = priority if priority in VALID_PRIORITIES else "normal"
|
|
|
|
|
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"INSERT INTO tasks (id, title, description, priority, assigned_to, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
|
|
|
(task_id, title, description, priority, assigned_to, now),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"INSERT INTO tasks (id, title, description, priority, assigned_to, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
|
|
|
(task_id, title, description, priority, assigned_to, now),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
raise HTTPException(status_code=503, detail="Task database unavailable") from exc
|
|
|
|
|
|
|
|
|
|
task = _TaskView(_row_to_dict(row))
|
|
|
|
|
return templates.TemplateResponse(request, "partials/task_card.html", {"task": task})
|
|
|
|
|
@@ -280,13 +300,17 @@ async def modify_task(
|
|
|
|
|
description: str = Form(""),
|
|
|
|
|
):
|
|
|
|
|
"""Update task title and description."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"UPDATE tasks SET title=?, description=? WHERE id=?",
|
|
|
|
|
(title, description, task_id),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"UPDATE tasks SET title=?, description=? WHERE id=?",
|
|
|
|
|
(title, description, task_id),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
raise HTTPException(status_code=503, detail="Task database unavailable") from exc
|
|
|
|
|
if not row:
|
|
|
|
|
raise HTTPException(404, "Task not found")
|
|
|
|
|
task = _TaskView(_row_to_dict(row))
|
|
|
|
|
@@ -298,13 +322,17 @@ async def _set_status(request: Request, task_id: str, new_status: str):
|
|
|
|
|
completed_at = (
|
|
|
|
|
datetime.now(UTC).isoformat() if new_status in ("completed", "vetoed", "failed") else None
|
|
|
|
|
)
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
|
|
|
|
|
(new_status, completed_at, task_id),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
|
|
|
|
|
(new_status, completed_at, task_id),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
raise HTTPException(status_code=503, detail="Task database unavailable") from exc
|
|
|
|
|
if not row:
|
|
|
|
|
raise HTTPException(404, "Task not found")
|
|
|
|
|
task = _TaskView(_row_to_dict(row))
|
|
|
|
|
@@ -330,22 +358,26 @@ async def api_create_task(request: Request):
|
|
|
|
|
if priority not in VALID_PRIORITIES:
|
|
|
|
|
priority = "normal"
|
|
|
|
|
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"INSERT INTO tasks (id, title, description, priority, assigned_to, created_by, created_at) "
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
|
|
|
(
|
|
|
|
|
task_id,
|
|
|
|
|
title,
|
|
|
|
|
body.get("description", ""),
|
|
|
|
|
priority,
|
|
|
|
|
body.get("assigned_to", ""),
|
|
|
|
|
body.get("created_by", "operator"),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"INSERT INTO tasks (id, title, description, priority, assigned_to, created_by, created_at) "
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
|
|
|
(
|
|
|
|
|
task_id,
|
|
|
|
|
title,
|
|
|
|
|
body.get("description", ""),
|
|
|
|
|
priority,
|
|
|
|
|
body.get("assigned_to", ""),
|
|
|
|
|
body.get("created_by", "operator"),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
raise HTTPException(status_code=503, detail="Task database unavailable") from exc
|
|
|
|
|
|
|
|
|
|
return JSONResponse(_row_to_dict(row), status_code=201)
|
|
|
|
|
|
|
|
|
|
@@ -353,8 +385,12 @@ async def api_create_task(request: Request):
|
|
|
|
|
@router.get("/api/tasks", response_class=JSONResponse)
|
|
|
|
|
async def api_list_tasks():
|
|
|
|
|
"""List all tasks as JSON."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute("SELECT * FROM tasks ORDER BY created_at DESC").fetchall()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
rows = db.execute("SELECT * FROM tasks ORDER BY created_at DESC").fetchall()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
return JSONResponse([], status_code=200)
|
|
|
|
|
return JSONResponse([_row_to_dict(r) for r in rows])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -369,13 +405,17 @@ async def api_update_status(task_id: str, request: Request):
|
|
|
|
|
completed_at = (
|
|
|
|
|
datetime.now(UTC).isoformat() if new_status in ("completed", "vetoed", "failed") else None
|
|
|
|
|
)
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
|
|
|
|
|
(new_status, completed_at, task_id),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
db.execute(
|
|
|
|
|
"UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
|
|
|
|
|
(new_status, completed_at, task_id),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
raise HTTPException(status_code=503, detail="Task database unavailable") from exc
|
|
|
|
|
if not row:
|
|
|
|
|
raise HTTPException(404, "Task not found")
|
|
|
|
|
return JSONResponse(_row_to_dict(row))
|
|
|
|
|
@@ -384,9 +424,13 @@ async def api_update_status(task_id: str, request: Request):
|
|
|
|
|
@router.delete("/api/tasks/{task_id}", response_class=JSONResponse)
|
|
|
|
|
async def api_delete_task(task_id: str):
|
|
|
|
|
"""Delete a task."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
cursor = db.execute("DELETE FROM tasks WHERE id=?", (task_id,))
|
|
|
|
|
db.commit()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
cursor = db.execute("DELETE FROM tasks WHERE id=?", (task_id,))
|
|
|
|
|
db.commit()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
raise HTTPException(status_code=503, detail="Task database unavailable") from exc
|
|
|
|
|
if cursor.rowcount == 0:
|
|
|
|
|
raise HTTPException(404, "Task not found")
|
|
|
|
|
return JSONResponse({"success": True, "id": task_id})
|
|
|
|
|
@@ -400,15 +444,19 @@ async def api_delete_task(task_id: str):
|
|
|
|
|
@router.get("/api/queue/status", response_class=JSONResponse)
|
|
|
|
|
async def queue_status(assigned_to: str = "default"):
|
|
|
|
|
"""Return queue status for the chat panel's agent status indicator."""
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
running = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status='running' AND assigned_to=? LIMIT 1",
|
|
|
|
|
(assigned_to,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
ahead = db.execute(
|
|
|
|
|
"SELECT COUNT(*) as cnt FROM tasks WHERE status IN ('pending_approval','approved') AND assigned_to=?",
|
|
|
|
|
(assigned_to,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
try:
|
|
|
|
|
with _get_db() as db:
|
|
|
|
|
running = db.execute(
|
|
|
|
|
"SELECT * FROM tasks WHERE status='running' AND assigned_to=? LIMIT 1",
|
|
|
|
|
(assigned_to,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
ahead = db.execute(
|
|
|
|
|
"SELECT COUNT(*) as cnt FROM tasks WHERE status IN ('pending_approval','approved') AND assigned_to=?",
|
|
|
|
|
(assigned_to,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
except sqlite3.OperationalError as exc:
|
|
|
|
|
logger.warning("Task DB unavailable: %s", exc)
|
|
|
|
|
return JSONResponse({"is_working": False, "current_task": None, "tasks_ahead": 0})
|
|
|
|
|
|
|
|
|
|
if running:
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|