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