🔧 Initial dev copy from live
This commit is contained in:
218
api/mc.py
Normal file
218
api/mc.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
Mission Control API — Data layer for Hermes WebUI Mission Control extension.
|
||||
Provides priorities, tasks, feed, and dashboard status management.
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from api.helpers import j
|
||||
|
||||
# ── State file ────────────────────────────────────────────────────────────────
|
||||
_MC_DATA_FILE = Path.home() / ".hermes" / "data" / "mc-data.json"
|
||||
_MC_LOCK = threading.RLock()
|
||||
|
||||
# ── Default structure ─────────────────────────────────────────────────────────
|
||||
DEFAULT_MC_DATA = {
|
||||
"priorities": [],
|
||||
"tasks": [],
|
||||
"feed": [],
|
||||
}
|
||||
|
||||
|
||||
def _load_mc_data() -> dict:
|
||||
"""Load Mission Control data from disk."""
|
||||
with _MC_LOCK:
|
||||
if not _MC_DATA_FILE.exists():
|
||||
return DEFAULT_MC_DATA.copy()
|
||||
try:
|
||||
with open(_MC_DATA_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return DEFAULT_MC_DATA.copy()
|
||||
|
||||
|
||||
def _save_mc_data(data: dict) -> None:
|
||||
"""Save Mission Control data to disk."""
|
||||
with _MC_LOCK:
|
||||
_MC_DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(_MC_DATA_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
# ── Priority helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_priorities() -> list[dict]:
|
||||
"""Return all priorities sorted by id."""
|
||||
data = _load_mc_data()
|
||||
return sorted(data.get("priorities", []), key=lambda p: p.get("id", 0))
|
||||
|
||||
|
||||
def create_priority(name: str, color: str = "#808080") -> dict:
|
||||
"""Add a new priority. Returns the created priority."""
|
||||
data = _load_mc_data()
|
||||
priorities = data.get("priorities", [])
|
||||
new_id = max([p.get("id", 0) for p in priorities], default=0) + 1
|
||||
priority = {"id": new_id, "name": name, "color": color}
|
||||
priorities.append(priority)
|
||||
data["priorities"] = priorities
|
||||
_save_mc_data(data)
|
||||
_add_feed_event(f"Priority created: {name}")
|
||||
return priority
|
||||
|
||||
|
||||
def update_priority(priority_id: int, name: str = None, color: str = None, done: bool = None) -> dict | None:
|
||||
"""Update an existing priority. Returns updated priority or None if not found."""
|
||||
data = _load_mc_data()
|
||||
priorities = data.get("priorities", [])
|
||||
for p in priorities:
|
||||
if p.get("id") == priority_id:
|
||||
if name is not None:
|
||||
p["name"] = name
|
||||
if color is not None:
|
||||
p["color"] = color
|
||||
if done is not None:
|
||||
p["done"] = done
|
||||
if done:
|
||||
_add_feed_event(f"Priority completed: {p['name']}")
|
||||
data["priorities"] = priorities
|
||||
_save_mc_data(data)
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def delete_priority(priority_id: int) -> bool:
|
||||
"""Delete a priority. Returns True if found and deleted."""
|
||||
data = _load_mc_data()
|
||||
priorities = data.get("priorities", [])
|
||||
original_len = len(priorities)
|
||||
priorities = [p for p in priorities if p.get("id") != priority_id]
|
||||
if len(priorities) < original_len:
|
||||
data["priorities"] = priorities
|
||||
_save_mc_data(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ── Task helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_tasks() -> list[dict]:
|
||||
"""Return all tasks sorted by priority then id."""
|
||||
data = _load_mc_data()
|
||||
return sorted(data.get("tasks", []), key=lambda t: (t.get("priority", 999), t.get("id", 0)))
|
||||
|
||||
|
||||
def create_task(title: str, priority: int = 1, status: str = "backlog") -> dict:
|
||||
"""Create a new task. Returns the created task."""
|
||||
data = _load_mc_data()
|
||||
tasks = data.get("tasks", [])
|
||||
new_id = max([t.get("id", 0) for t in tasks], default=0) + 1
|
||||
task = {"id": new_id, "title": title, "priority": priority, "status": status}
|
||||
tasks.append(task)
|
||||
data["tasks"] = tasks
|
||||
_save_mc_data(data)
|
||||
_add_feed_event(f"Task created: {title}")
|
||||
return task
|
||||
|
||||
|
||||
def update_task(task_id: int, **kwargs) -> dict | None:
|
||||
"""Update a task by id. kwargs: title, priority, status. Returns updated task or None."""
|
||||
data = _load_mc_data()
|
||||
tasks = data.get("tasks", [])
|
||||
for t in tasks:
|
||||
if t.get("id") == task_id:
|
||||
old_status = t.get("status")
|
||||
for key in ("title", "priority", "status"):
|
||||
if key in kwargs:
|
||||
t[key] = kwargs[key]
|
||||
new_status = t.get("status")
|
||||
# Feed events for status transitions
|
||||
if old_status != new_status:
|
||||
if new_status == "done":
|
||||
_add_feed_event(f"Task completed: {t['title']}")
|
||||
elif new_status == "progress":
|
||||
_add_feed_event(f"Task started: {t['title']}")
|
||||
data["tasks"] = tasks
|
||||
_save_mc_data(data)
|
||||
return t
|
||||
return None
|
||||
|
||||
|
||||
def delete_task(task_id: int) -> bool:
|
||||
"""Delete a task. Returns True if found and deleted."""
|
||||
data = _load_mc_data()
|
||||
tasks = data.get("tasks", [])
|
||||
original_len = len(tasks)
|
||||
tasks = [t for t in tasks if t.get("id") != task_id]
|
||||
if len(tasks) < original_len:
|
||||
data["tasks"] = tasks
|
||||
_save_mc_data(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ── Feed helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_feed(limit: int = 50) -> list[dict]:
|
||||
"""Return recent feed events, newest first."""
|
||||
data = _load_mc_data()
|
||||
feed = data.get("feed", [])
|
||||
return sorted(feed, key=lambda f: f.get("timestamp", ""), reverse=True)[:limit]
|
||||
|
||||
|
||||
def _add_feed_event(event: str) -> None:
|
||||
"""Add a timestamped feed event."""
|
||||
data = _load_mc_data()
|
||||
feed = data.get("feed", [])
|
||||
feed.append({
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"event": event,
|
||||
})
|
||||
# Keep only last 200 events
|
||||
data["feed"] = feed[-200:]
|
||||
_save_mc_data(data)
|
||||
|
||||
|
||||
# ── Dashboard status ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_dashboard_status() -> dict:
|
||||
"""Return aggregated dashboard status for Mission Control."""
|
||||
data = _load_mc_data()
|
||||
priorities = data.get("priorities", [])
|
||||
tasks = data.get("tasks", [])
|
||||
|
||||
priorities_total = len(priorities)
|
||||
priorities_done = sum(1 for p in priorities if p.get("done"))
|
||||
|
||||
tasks_backlog = sum(1 for t in tasks if t.get("status") == "backlog")
|
||||
tasks_progress = sum(1 for t in tasks if t.get("status") == "progress")
|
||||
tasks_done = sum(1 for t in tasks if t.get("status") == "done")
|
||||
|
||||
feed = get_feed(limit=5)
|
||||
latest_event = feed[0]["event"] if feed else "No recent activity"
|
||||
|
||||
# Health assessment
|
||||
if tasks_done == 0 and tasks_backlog == 0 and tasks_progress == 0:
|
||||
health = "empty"
|
||||
elif tasks_progress > 0 and tasks_done > 0:
|
||||
health = "healthy"
|
||||
elif tasks_progress > 0:
|
||||
health = "active"
|
||||
elif tasks_backlog > 0:
|
||||
health = "warning"
|
||||
else:
|
||||
health = "ok"
|
||||
|
||||
return {
|
||||
"priorities_total": priorities_total,
|
||||
"priorities_done": priorities_done,
|
||||
"tasks_backlog": tasks_backlog,
|
||||
"tasks_progress": tasks_progress,
|
||||
"tasks_done": tasks_done,
|
||||
"latest_feed_event": latest_event,
|
||||
"dashboard_health": health,
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
Reference in New Issue
Block a user