Files
webui-develop/api/mc.py

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,
}