696 lines
28 KiB
Python
696 lines
28 KiB
Python
# api/mc.py
|
|
# Mission Control — Projects & Tasks API
|
|
# Rose's persönliches PM-System
|
|
|
|
import json
|
|
import uuid
|
|
from pathlib import Path
|
|
from datetime import datetime, date, timedelta
|
|
|
|
HERMES_HOME = Path.home() / ".hermes"
|
|
DATA_DIR = HERMES_HOME / "data" / "mc"
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
TASKS_FILE = DATA_DIR / "tasks.json"
|
|
PROJECTS_FILE = DATA_DIR / "projects.json"
|
|
|
|
TASKS_FILE.write_text(json.dumps({"version": "3.0.0", "tasks": []}, indent=2))
|
|
PROJECTS_FILE.write_text(json.dumps({"version": "3.0.0", "projects": []}, indent=2))
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# AGENT REGISTRY
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
AGENTS = {
|
|
"root": {"name": "🌳 Root", "emoji": "🌳", "domain": "Infrastruktur, Server, Docker, Backups"},
|
|
"forget-me-not": {"name": "🌼 Forget-me-not", "emoji": "🌼", "domain": "Kalender, Termine, Geburtstage"},
|
|
"sunflower": {"name": "🌻 Sunflower", "emoji": "🌻", "domain": "Finanzen, Abos, Rechnungen"},
|
|
"iris": {"name": "⚜️ Iris", "emoji": "⚜️", "domain": "Karriere, Lernen, Focus"},
|
|
"lotus": {"name": "🪷 Lotus", "emoji": "🪷", "domain": "Gesundheit, Fitness, Hobbys"},
|
|
"ivy": {"name": "🌿 Ivy", "emoji": "🌿", "domain": "Smart Home, Home Assistant"},
|
|
"dandelion": {"name": "🛡 Dandelion", "emoji": "🛡", "domain": "Kommunikation, Notifications, Spam"},
|
|
"rose": {"name": "🌹 Rose", "emoji": "🌹", "domain": "Orchestrierung, Koordination"},
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# INTERNAL HELPERS
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def _load_tasks():
|
|
if TASKS_FILE.exists():
|
|
with TASKS_FILE.open(encoding="utf-8") as f:
|
|
return json.loads(f.read())
|
|
return {"version": "3.0.0", "tasks": []}
|
|
|
|
def _save_tasks(data):
|
|
TASKS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
|
|
def _load_projects():
|
|
if PROJECTS_FILE.exists():
|
|
with PROJECTS_FILE.open(encoding="utf-8") as f:
|
|
return json.loads(f.read())
|
|
return {"version": "3.0.0", "projects": []}
|
|
|
|
def _save_projects(data):
|
|
PROJECTS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
|
|
def _new_id(prefix="task"):
|
|
return f"{prefix}-{datetime.now().strftime('%y%m%d%H%M%S')}-{uuid.uuid4().hex[:4]}"
|
|
|
|
def _now():
|
|
return datetime.now().isoformat()
|
|
|
|
def _today():
|
|
return date.today().isoformat()
|
|
|
|
def _auto_done_subtasks(item):
|
|
"""Check if all subtasks are done (for auto-done logic)."""
|
|
subtasks = item.get("subtasks", [])
|
|
if not subtasks:
|
|
return None
|
|
all_done = all(s.get("done", False) for s in subtasks)
|
|
return all_done
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# TASKS — CRUD
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def list_tasks(filters=None):
|
|
"""GET /api/mc/tasks — alle Tasks mit optionalen Filtern."""
|
|
data = _load_tasks()
|
|
tasks = data.get("tasks", [])
|
|
|
|
if not filters:
|
|
return tasks
|
|
|
|
# Filter: project_id
|
|
if "project_id" in filters and filters["project_id"]:
|
|
tasks = [t for t in tasks if t.get("project_id") == filters["project_id"]]
|
|
|
|
# Filter: phase_id
|
|
if "phase_id" in filters and filters["phase_id"]:
|
|
tasks = [t for t in tasks if t.get("phase_id") == filters["phase_id"]]
|
|
|
|
# Filter: task_type
|
|
if "task_type" in filters and filters["task_type"]:
|
|
tasks = [t for t in tasks if t.get("task_type") == filters["task_type"]]
|
|
|
|
# Filter: type (user/agent)
|
|
if "type" in filters and filters["type"]:
|
|
tasks = [t for t in tasks if t.get("type") == filters["type"]]
|
|
|
|
# Filter: assigned_agent
|
|
if "assigned_agent" in filters and filters["assigned_agent"]:
|
|
tasks = [t for t in tasks if t.get("assigned_agent") == filters["assigned_agent"]]
|
|
|
|
# Filter: status
|
|
if "status" in filters and filters["status"]:
|
|
tasks = [t for t in tasks if t.get("status") == filters["status"]]
|
|
|
|
# Filter: priority
|
|
if "priority" in filters and filters["priority"]:
|
|
tasks = [t for t in tasks if t.get("priority") == filters["priority"]]
|
|
|
|
# Filter: task_type = one-time | daily (shorthand)
|
|
if "task_type" in filters and filters["task_type"]:
|
|
tasks = [t for t in tasks if t.get("task_type") == filters["task_type"]]
|
|
|
|
return tasks
|
|
|
|
def get_task(task_id):
|
|
"""GET /api/mc/tasks/:id — einzelner Task."""
|
|
data = _load_tasks()
|
|
return next((t for t in data["tasks"] if t["id"] == task_id), None)
|
|
|
|
def create_task(body):
|
|
"""POST /api/mc/tasks — Task erstellen."""
|
|
data = _load_tasks()
|
|
|
|
task = {
|
|
"id": _new_id("task"),
|
|
"title": body.get("title", "Untitled Task"),
|
|
"task_type": body.get("task_type", "one-time"),
|
|
"type": body.get("type", "user"),
|
|
"project_id": body.get("project_id"),
|
|
"phase_id": body.get("phase_id"),
|
|
"status": body.get("status", "todo"),
|
|
"priority": body.get("priority", "p2"),
|
|
"due": body.get("due"),
|
|
"due_time": body.get("due_time"),
|
|
"tags": body.get("tags", []),
|
|
"daily_schedule": body.get("daily_schedule"),
|
|
"daily_completed_today": False,
|
|
"daily_last_done": None,
|
|
"assigned_agent": body.get("assigned_agent"),
|
|
"agent_status": "pending" if body.get("type") == "agent" else None,
|
|
"agent_note": body.get("agent_note"),
|
|
"cron_schedule": body.get("cron_schedule"),
|
|
"cron_last_run": None,
|
|
"cron_next_run": None,
|
|
"subtasks": [],
|
|
"created_by": body.get("created_by", "user"),
|
|
"created_at": _now(),
|
|
"updated_at": _now(),
|
|
"completed_at": None,
|
|
}
|
|
|
|
data["tasks"].append(task)
|
|
_save_tasks(data)
|
|
return task
|
|
|
|
def update_task(task_id, body):
|
|
"""PUT /api/mc/tasks/:id — Task updaten."""
|
|
data = _load_tasks()
|
|
|
|
for t in data["tasks"]:
|
|
if t["id"] == task_id:
|
|
# Erlaubte Felder
|
|
for key in ["title", "task_type", "type", "project_id", "phase_id",
|
|
"status", "priority", "due", "due_time", "tags",
|
|
"daily_schedule", "assigned_agent", "agent_status",
|
|
"agent_note", "cron_schedule", "cron_last_run",
|
|
"cron_next_run", "daily_completed_today", "daily_last_done"]:
|
|
if key in body:
|
|
t[key] = body[key]
|
|
|
|
# Status → completed_at
|
|
if body.get("status") == "done" and t["completed_at"] is None:
|
|
t["completed_at"] = _now()
|
|
elif body.get("status") and body.get("status") != "done":
|
|
t["completed_at"] = None
|
|
|
|
t["updated_at"] = _now()
|
|
|
|
# Auto-done via subtasks
|
|
all_done = _auto_done_subtasks(t)
|
|
if all_done is True and t["status"] != "done":
|
|
t["status"] = "done"
|
|
t["completed_at"] = _now()
|
|
elif all_done is False and t["status"] == "done":
|
|
t["status"] = "todo"
|
|
|
|
_save_tasks(data)
|
|
return t
|
|
|
|
return None
|
|
|
|
def delete_task(task_id):
|
|
"""DELETE /api/mc/tasks/:id — Task löschen."""
|
|
data = _load_tasks()
|
|
before = len(data["tasks"])
|
|
data["tasks"] = [t for t in data["tasks"] if t["id"] != task_id]
|
|
_save_tasks(data)
|
|
return len(data["tasks"]) < before
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# DAILY TASKS
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def list_daily():
|
|
"""GET /api/mc/daily — alle Daily Tasks."""
|
|
return list_tasks({"task_type": "daily"})
|
|
|
|
def toggle_daily_done(task_id):
|
|
"""POST /api/mc/daily/:id/done — Daily Task heute erledigt togglen."""
|
|
data = _load_tasks()
|
|
|
|
for t in data["tasks"]:
|
|
if t["id"] == task_id and t.get("task_type") == "daily":
|
|
t["daily_completed_today"] = not t["daily_completed_today"]
|
|
if t["daily_completed_today"]:
|
|
t["daily_last_done"] = _today()
|
|
t["status"] = "done"
|
|
t["completed_at"] = _now()
|
|
else:
|
|
t["status"] = "todo"
|
|
t["completed_at"] = None
|
|
t["updated_at"] = _now()
|
|
_save_tasks(data)
|
|
return t
|
|
|
|
return None
|
|
|
|
def reset_daily_tasks():
|
|
"""POST /api/mc/daily/reset — Alle daily_completed_today = false (Mitternacht)."""
|
|
data = _load_tasks()
|
|
for t in data["tasks"]:
|
|
if t.get("task_type") == "daily":
|
|
t["daily_completed_today"] = False
|
|
if t["status"] == "done" and t.get("daily_last_done") != _today():
|
|
t["status"] = "todo"
|
|
t["completed_at"] = None
|
|
_save_tasks(data)
|
|
return {"ok": True, "reset_at": _now()}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# SUBTASKS — TASK LEVEL
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def add_subtask_task(task_id, body):
|
|
"""POST /api/mc/tasks/:id/subtasks — Subtask zu Task."""
|
|
data = _load_tasks()
|
|
for t in data["tasks"]:
|
|
if t["id"] == task_id:
|
|
subtask = {
|
|
"id": _new_id("sub"),
|
|
"title": body.get("title", "Subtask"),
|
|
"done": False,
|
|
"order": len(t.get("subtasks", [])) + 1,
|
|
"created_at": _now(),
|
|
}
|
|
if "subtasks" not in t:
|
|
t["subtasks"] = []
|
|
t["subtasks"].append(subtask)
|
|
t["updated_at"] = _now()
|
|
_save_tasks(data)
|
|
return subtask
|
|
return None
|
|
|
|
def update_subtask_task(task_id, subtask_id, body):
|
|
"""PUT /api/mc/tasks/:id/subtasks/:sid — Subtask updaten."""
|
|
data = _load_tasks()
|
|
for t in data["tasks"]:
|
|
if t["id"] == task_id:
|
|
for s in t.get("subtasks", []):
|
|
if s["id"] == subtask_id:
|
|
if "title" in body:
|
|
s["title"] = body["title"]
|
|
if "done" in body:
|
|
s["done"] = body["done"]
|
|
if "order" in body:
|
|
s["order"] = body["order"]
|
|
t["updated_at"] = _now()
|
|
|
|
# Auto-done check
|
|
all_done = _auto_done_subtasks(t)
|
|
if all_done is True and t["status"] != "done":
|
|
t["status"] = "done"
|
|
t["completed_at"] = _now()
|
|
elif all_done is False and t["status"] == "done":
|
|
t["status"] = "todo"
|
|
t["completed_at"] = None
|
|
|
|
_save_tasks(data)
|
|
return s
|
|
return None
|
|
|
|
def delete_subtask_task(task_id, subtask_id):
|
|
"""DELETE /api/mc/tasks/:id/subtasks/:sid — Subtask löschen."""
|
|
data = _load_tasks()
|
|
for t in data["tasks"]:
|
|
if t["id"] == task_id:
|
|
before = len(t.get("subtasks", []))
|
|
t["subtasks"] = [s for s in t.get("subtasks", []) if s["id"] != subtask_id]
|
|
t["updated_at"] = _now()
|
|
_save_tasks(data)
|
|
return len(t["subtasks"]) < before
|
|
return False
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# PROJECTS — CRUD
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def list_projects():
|
|
"""GET /api/mc/projects — alle Projekte mit Phasen."""
|
|
data = _load_projects()
|
|
return data.get("projects", [])
|
|
|
|
def get_project(project_id):
|
|
"""GET /api/mc/projects/:id — einzelnes Projekt."""
|
|
data = _load_projects()
|
|
return next((p for p in data["projects"] if p["id"] == project_id), None)
|
|
|
|
def create_project(body):
|
|
"""POST /api/mc/projects — Projekt erstellen."""
|
|
data = _load_projects()
|
|
|
|
project = {
|
|
"id": body.get("id") or _new_id("proj").replace("proj-", ""),
|
|
"name": body.get("name", "Neues Projekt"),
|
|
"color": body.get("color", "#6366f1"),
|
|
"description": body.get("description", ""),
|
|
"status": body.get("status", "active"),
|
|
"created_at": _now(),
|
|
"updated_at": _now(),
|
|
"subtasks": [],
|
|
"phases": [],
|
|
}
|
|
|
|
data["projects"].append(project)
|
|
_save_projects(data)
|
|
return project
|
|
|
|
def update_project(project_id, body):
|
|
"""PUT /api/mc/projects/:id — Projekt updaten."""
|
|
data = _load_projects()
|
|
|
|
for p in data["projects"]:
|
|
if p["id"] == project_id:
|
|
for key in ["name", "color", "description", "status"]:
|
|
if key in body:
|
|
p[key] = body[key]
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return p
|
|
|
|
return None
|
|
|
|
def delete_project(project_id):
|
|
"""DELETE /api/mc/projects/:id — Projekt löschen."""
|
|
data = _load_projects()
|
|
before = len(data["projects"])
|
|
data["projects"] = [p for p in data["projects"] if p["id"] != project_id]
|
|
_save_projects(data)
|
|
|
|
# Auch alle Tasks dieses Projekts löschen
|
|
tasks_data = _load_tasks()
|
|
tasks_data["tasks"] = [t for t in tasks_data["tasks"] if t.get("project_id") != project_id]
|
|
_save_tasks(tasks_data)
|
|
|
|
return len(data["projects"]) < before
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# PROJECT SUBTASKS
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def add_subtask_project(project_id, body):
|
|
"""POST /api/mc/projects/:id/subtasks."""
|
|
data = _load_projects()
|
|
for p in data["projects"]:
|
|
if p["id"] == project_id:
|
|
subtask = {
|
|
"id": _new_id("sub"),
|
|
"title": body.get("title", "Subtask"),
|
|
"done": False,
|
|
"order": len(p.get("subtasks", [])) + 1,
|
|
"created_at": _now(),
|
|
}
|
|
if "subtasks" not in p:
|
|
p["subtasks"] = []
|
|
p["subtasks"].append(subtask)
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return subtask
|
|
return None
|
|
|
|
def update_subtask_project(project_id, subtask_id, body):
|
|
"""PUT /api/mc/projects/:id/subtasks/:sid."""
|
|
data = _load_projects()
|
|
for p in data["projects"]:
|
|
if p["id"] == project_id:
|
|
for s in p.get("subtasks", []):
|
|
if s["id"] == subtask_id:
|
|
if "title" in body:
|
|
s["title"] = body["title"]
|
|
if "done" in body:
|
|
s["done"] = body["done"]
|
|
if "order" in body:
|
|
s["order"] = body["order"]
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return s
|
|
return None
|
|
|
|
def delete_subtask_project(project_id, subtask_id):
|
|
"""DELETE /api/mc/projects/:id/subtasks/:sid."""
|
|
data = _load_projects()
|
|
for p in data["projects"]:
|
|
if p["id"] == project_id:
|
|
before = len(p.get("subtasks", []))
|
|
p["subtasks"] = [s for s in p.get("subtasks", []) if s["id"] != subtask_id]
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return len(p["subtasks"]) < before
|
|
return False
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# PHASES
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def add_phase(project_id, body):
|
|
"""POST /api/mc/projects/:id/phases — Phase hinzufügen."""
|
|
data = _load_projects()
|
|
|
|
for p in data["projects"]:
|
|
if p["id"] == project_id:
|
|
phase = {
|
|
"id": _new_id("phase"),
|
|
"name": body.get("name", "Neue Phase"),
|
|
"description": body.get("description", ""),
|
|
"testing": body.get("testing", ""),
|
|
"testing_status": body.get("testing_status", "pending"),
|
|
"reflection": body.get("reflection"),
|
|
"status": body.get("status", "todo"),
|
|
"order": len(p.get("phases", [])) + 1,
|
|
"completed_at": None,
|
|
"subtasks": [],
|
|
}
|
|
if "phases" not in p:
|
|
p["phases"] = []
|
|
p["phases"].append(phase)
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return phase
|
|
|
|
return None
|
|
|
|
def update_phase(phase_id, body):
|
|
"""PUT /api/mc/phases/:id — Phase updaten."""
|
|
data = _load_projects()
|
|
|
|
for p in data["projects"]:
|
|
for ph in p.get("phases", []):
|
|
if ph["id"] == phase_id:
|
|
for key in ["name", "description", "testing", "testing_status",
|
|
"reflection", "status", "order"]:
|
|
if key in body:
|
|
ph[key] = body[key]
|
|
|
|
if body.get("status") == "done" and ph["completed_at"] is None:
|
|
ph["completed_at"] = _now()
|
|
elif body.get("status") and body.get("status") != "done":
|
|
ph["completed_at"] = None
|
|
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return ph
|
|
|
|
return None
|
|
|
|
def delete_phase(phase_id):
|
|
"""DELETE /api/mc/phases/:id — Phase löschen."""
|
|
data = _load_projects()
|
|
|
|
for p in data["projects"]:
|
|
before = len(p.get("phases", []))
|
|
p["phases"] = [ph for ph in p.get("phases", []) if ph["id"] != phase_id]
|
|
if len(p["phases"]) < before:
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
|
|
# Tasks dieser Phase auf standalone setzen
|
|
tasks_data = _load_tasks()
|
|
for t in tasks_data["tasks"]:
|
|
if t.get("phase_id") == phase_id:
|
|
t["phase_id"] = None
|
|
_save_tasks(tasks_data)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
def complete_phase(phase_id):
|
|
"""PUT /api/mc/phases/:id/complete — Phase als done markieren."""
|
|
return update_phase(phase_id, {"status": "done", "completed_at": _now()})
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# PHASE SUBTASKS
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def add_subtask_phase(phase_id, body):
|
|
"""POST /api/mc/phases/:id/subtasks."""
|
|
data = _load_projects()
|
|
|
|
for p in data["projects"]:
|
|
for ph in p.get("phases", []):
|
|
if ph["id"] == phase_id:
|
|
subtask = {
|
|
"id": _new_id("sub"),
|
|
"title": body.get("title", "Subtask"),
|
|
"done": False,
|
|
"order": len(ph.get("subtasks", [])) + 1,
|
|
"created_at": _now(),
|
|
}
|
|
if "subtasks" not in ph:
|
|
ph["subtasks"] = []
|
|
ph["subtasks"].append(subtask)
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return subtask
|
|
|
|
return None
|
|
|
|
def update_subtask_phase(phase_id, subtask_id, body):
|
|
"""PUT /api/mc/phases/:id/subtasks/:sid."""
|
|
data = _load_projects()
|
|
|
|
for p in data["projects"]:
|
|
for ph in p.get("phases", []):
|
|
if ph["id"] == phase_id:
|
|
for s in ph.get("subtasks", []):
|
|
if s["id"] == subtask_id:
|
|
if "title" in body:
|
|
s["title"] = body["title"]
|
|
if "done" in body:
|
|
s["done"] = body["done"]
|
|
if "order" in body:
|
|
s["order"] = body["order"]
|
|
p["updated_at"] = _now()
|
|
|
|
# Auto-done check für phase
|
|
all_done = all(st.get("done", False) for st in ph.get("subtasks", []))
|
|
if all_done and ph["status"] != "done":
|
|
ph["status"] = "done"
|
|
ph["completed_at"] = _now()
|
|
|
|
_save_projects(data)
|
|
return s
|
|
return None
|
|
|
|
def delete_subtask_phase(phase_id, subtask_id):
|
|
"""DELETE /api/mc/phases/:id/subtasks/:sid."""
|
|
data = _load_projects()
|
|
|
|
for p in data["projects"]:
|
|
for ph in p.get("phases", []):
|
|
if ph["id"] == phase_id:
|
|
before = len(ph.get("subtasks", []))
|
|
ph["subtasks"] = [s for s in ph.get("subtasks", []) if s["id"] != subtask_id]
|
|
p["updated_at"] = _now()
|
|
_save_projects(data)
|
|
return len(ph["subtasks"]) < before
|
|
|
|
return False
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# AGENT ACTIONS
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def agent_progress(task_id, body):
|
|
"""POST /api/mc/tasks/:id/progress — Agent meldet Fortschritt."""
|
|
return update_task(task_id, {
|
|
"agent_status": body.get("agent_status"),
|
|
"agent_note": body.get("agent_note"),
|
|
"cron_last_run": body.get("cron_last_run"),
|
|
"cron_next_run": body.get("cron_next_run"),
|
|
})
|
|
|
|
def agent_note(task_id, body):
|
|
"""POST /api/mc/tasks/:id/note — Agent setzt Notiz."""
|
|
return update_task(task_id, {"agent_note": body.get("note")})
|
|
|
|
def get_agents():
|
|
"""GET /api/mc/agents — Agent-Registry."""
|
|
return AGENTS
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# STATS
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
def get_stats():
|
|
"""GET /api/mc/stats — Statistiken."""
|
|
tasks_data = _load_tasks()
|
|
projects_data = _load_projects()
|
|
tasks = tasks_data.get("tasks", [])
|
|
projects = projects_data.get("projects", [])
|
|
|
|
total = len(tasks)
|
|
done = len([t for t in tasks if t.get("status") == "done"])
|
|
today = _today()
|
|
|
|
# Overdue
|
|
overdue = len([t for t in tasks if t.get("due") and t["due"] < today and t.get("status") != "done"])
|
|
|
|
# By priority
|
|
by_priority = {"p1": 0, "p2": 0, "p3": 0}
|
|
for t in tasks:
|
|
p = t.get("priority", "p2")
|
|
if p in by_priority:
|
|
by_priority[p] += 1
|
|
|
|
# By type
|
|
by_type = {"one-time": 0, "daily": 0}
|
|
for t in tasks:
|
|
tt = t.get("task_type", "one-time")
|
|
if tt in by_type:
|
|
by_type[tt] += 1
|
|
|
|
# By status
|
|
by_status = {"todo": 0, "in_progress": 0, "review": 0, "done": 0}
|
|
for t in tasks:
|
|
s = t.get("status", "todo")
|
|
if s in by_status:
|
|
by_status[s] += 1
|
|
|
|
# Daily done today
|
|
daily_done_today = len([t for t in tasks if t.get("task_type") == "daily" and t.get("daily_completed_today", False)])
|
|
|
|
# Streak: consecutive days with completions going backwards
|
|
streak = 0
|
|
check_date = date.today()
|
|
done_dates = set()
|
|
for t in tasks:
|
|
c = t.get("completed_at")
|
|
if c:
|
|
done_dates.add(c[:10])
|
|
while True:
|
|
d = check_date.isoformat()
|
|
if d in done_dates:
|
|
streak += 1
|
|
check_date -= timedelta(days=1)
|
|
else:
|
|
break
|
|
|
|
# Agent activity
|
|
agent_activity = {}
|
|
for agent_id, agent in AGENTS.items():
|
|
agent_tasks = [t for t in tasks if t.get("assigned_agent") == agent_id]
|
|
active = next((t for t in agent_tasks if t.get("agent_status") in ("running", "pending")), None)
|
|
agent_activity[agent_id] = {
|
|
**agent,
|
|
"task_count": len(agent_tasks),
|
|
"status": "active" if active else "idle",
|
|
"current_task": active["title"] if active else None,
|
|
}
|
|
|
|
# Project progress
|
|
project_progress = []
|
|
for pr in projects:
|
|
pr_tasks = [t for t in tasks if t.get("project_id") == pr["id"]]
|
|
pr_tasks_done = len([t for t in pr_tasks if t.get("status") == "done"])
|
|
phases_total = len(pr.get("phases", []))
|
|
phases_done = len([ph for ph in pr.get("phases", []) if ph.get("status") == "done"])
|
|
project_progress.append({
|
|
"id": pr["id"],
|
|
"name": pr["name"],
|
|
"color": pr.get("color"),
|
|
"status": pr.get("status"),
|
|
"tasks_total": len(pr_tasks),
|
|
"tasks_done": pr_tasks_done,
|
|
"phases_total": phases_total,
|
|
"phases_done": phases_done,
|
|
})
|
|
|
|
return {
|
|
"total": total,
|
|
"done": done,
|
|
"overdue": overdue,
|
|
"streak": streak,
|
|
"daily_done_today": daily_done_today,
|
|
"by_priority": by_priority,
|
|
"by_type": by_type,
|
|
"by_status": by_status,
|
|
"project_progress": project_progress,
|
|
"agent_activity": agent_activity,
|
|
}
|