🚀 Initial commit: Rose's custom WebUI with modernization + agent attribution
This commit is contained in:
150
api/agents.py
Normal file
150
api/agents.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Rose Agents Panel API — Data layer for Hermes WebUI Agents extension.
|
||||
Provides Rose + Tier-2 agent status, inbox management, and configuration.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from api.helpers import j
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────────────
|
||||
_HERMES_DIR = Path.home() / ".hermes"
|
||||
_AGENTS_DIR = _HERMES_DIR / "agents"
|
||||
_INBOX_BUS = _HERMES_DIR / "scripts" / "message_bus.py"
|
||||
|
||||
# ── Tier-2 Agent Registry ──────────────────────────────────────────────────────
|
||||
TIER2_AGENTS = {
|
||||
"lotus": {"name": "Lotus", "emoji": "🪷", "domain": "Health, Fitness & Recovery", "color": "#e91e63"},
|
||||
"forget-me-not": {"name": "Forget-me-not", "emoji": "🌼", "domain": "Calendar, Time & Social", "color": "#ff9800"},
|
||||
"sunflower": {"name": "Sunflower", "emoji": "🌻", "domain": "Finance, Wealth & Subscriptions","color": "#ffeb3b"},
|
||||
"iris": {"name": "Iris", "emoji": "⚜️", "domain": "Career, Learning & Focus", "color": "#9c27b0"},
|
||||
"ivy": {"name": "Ivy", "emoji": "🌿", "domain": "Smart Home & Environment", "color": "#4caf50"},
|
||||
"dandelion": {"name": "Dandelion", "emoji": "🛡️", "domain": "Communication Triage", "color": "#03a9f4"},
|
||||
"root": {"name": "Root", "emoji": "🌳", "domain": "DevOps, Logs & System Health", "color": "#795548"},
|
||||
}
|
||||
|
||||
ROSE_META = {
|
||||
"name": "Rose",
|
||||
"emoji": "🌹",
|
||||
"domain": "Orchestrator & Main Interface",
|
||||
"color": "#f44336",
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _get_process_status(agent_name: str) -> dict:
|
||||
"""Check if an agent process is running via ps."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", f"hermes.*--agent\\s+{agent_name}|message_bus.*--agent\\s+{agent_name}"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
running = bool(result.stdout.strip())
|
||||
pid = int(result.stdout.strip().split()[0]) if running else None
|
||||
return {"running": running, "pid": pid}
|
||||
except Exception:
|
||||
return {"running": False, "pid": None}
|
||||
|
||||
def _get_inbox_count(agent_name: str) -> int:
|
||||
"""Count messages in agent inbox via message_bus.py."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["/usr/bin/python3", str(_INBOX_BUS), "check", "--agent", agent_name],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
data = json.loads(result.stdout)
|
||||
return data.get("pending", 0)
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def _read_inbox(agent_name: str, limit: int = 20) -> list[dict]:
|
||||
"""Read messages from agent inbox."""
|
||||
inbox_path = _AGENTS_DIR / agent_name / "inbox.json"
|
||||
if not inbox_path.exists():
|
||||
return []
|
||||
try:
|
||||
with open(inbox_path, "r") as f:
|
||||
data = json.load(f)
|
||||
messages = data if isinstance(data, list) else data.get("messages", [])
|
||||
return messages[-limit:]
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return []
|
||||
|
||||
# ── API Functions ─────────────────────────────────────────────────────────────
|
||||
|
||||
def list_agents() -> dict:
|
||||
"""Return status for Rose + all Tier-2 agents."""
|
||||
agents = []
|
||||
|
||||
# Rose (the orchestrator)
|
||||
rose_running = True # Rose IS the gateway/webui
|
||||
rose_inbox_count = _get_inbox_count("rose")
|
||||
agents.append({
|
||||
"id": "rose",
|
||||
"name": ROSE_META["name"],
|
||||
"emoji": ROSE_META["emoji"],
|
||||
"domain": ROSE_META["domain"],
|
||||
"color": ROSE_META["color"],
|
||||
"tier": "orchestrator",
|
||||
"running": rose_running,
|
||||
"pid": None,
|
||||
"inbox_count": rose_inbox_count,
|
||||
})
|
||||
|
||||
# Tier-2 agents
|
||||
for agent_id, meta in TIER2_AGENTS.items():
|
||||
status = _get_process_status(agent_id)
|
||||
inbox_count = _get_inbox_count(agent_id) if status["running"] else 0
|
||||
agents.append({
|
||||
"id": agent_id,
|
||||
"name": meta["name"],
|
||||
"emoji": meta["emoji"],
|
||||
"domain": meta["domain"],
|
||||
"color": meta["color"],
|
||||
"tier": "tier2",
|
||||
"running": status["running"],
|
||||
"pid": status["pid"],
|
||||
"inbox_count": inbox_count,
|
||||
})
|
||||
|
||||
return {"agents": agents}
|
||||
|
||||
def get_agent_inbox(agent_id: str, limit: int = 20) -> dict:
|
||||
"""Return inbox messages for a specific agent."""
|
||||
if agent_id not in TIER2_AGENTS and agent_id != "rose":
|
||||
return {"error": f"Unknown agent: {agent_id}"}
|
||||
|
||||
messages = _read_inbox(agent_id, limit)
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"agent_name": TIER2_AGENTS.get(agent_id, {}).get("name", "Rose"),
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
def get_agent_config(agent_id: str) -> dict:
|
||||
"""Return configuration for a specific agent (soul.md path, etc)."""
|
||||
if agent_id == "rose":
|
||||
return {
|
||||
"id": "rose",
|
||||
"name": "Rose",
|
||||
"soul_path": str(_HERMES_DIR / "rose.md"),
|
||||
"memory_path": str(_HERMES_DIR / "memory.json"),
|
||||
}
|
||||
elif agent_id in TIER2_AGENTS:
|
||||
soul_path = _AGENTS_DIR / agent_id / "soul.md"
|
||||
inbox_path = _AGENTS_DIR / agent_id / "inbox.json"
|
||||
return {
|
||||
"id": agent_id,
|
||||
"name": TIER2_AGENTS[agent_id]["name"],
|
||||
"soul_path": str(soul_path) if soul_path.exists() else None,
|
||||
"inbox_path": str(inbox_path),
|
||||
}
|
||||
return {"error": f"Unknown agent: {agent_id}"}
|
||||
265
api/gateways.py
Normal file
265
api/gateways.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
Gateway management API for Hermes WebUI.
|
||||
|
||||
Provides endpoints to list, start, stop, restart, and add gateway connections
|
||||
like Telegram, OpenClaw, and other Hermes gateway types.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# In-memory gateway registry (gateway_name -> info)
|
||||
_gateways: dict[str, dict] = {}
|
||||
_gateways_lock = threading.Lock()
|
||||
|
||||
# Track running gateway processes (gateway_name -> PID)
|
||||
_gateway_pids: dict[str, int] = {}
|
||||
|
||||
|
||||
def _get_hermes_home() -> Path:
|
||||
"""Get the Hermes home directory."""
|
||||
hermes_home = os.environ.get("HERMES_HOME", "")
|
||||
if hermes_home and str(Path(hermes_home).parent) != "profiles":
|
||||
return Path(hermes_home)
|
||||
return Path.home() / ".hermes"
|
||||
|
||||
|
||||
def _get_gateway_pid(name: str) -> Optional[int]:
|
||||
"""Get PID of a running gateway process by name."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", f"hermes.*gateway.*{name}"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
pids = result.stdout.strip().split("\n")
|
||||
return int(pids[0]) if pids else None
|
||||
except Exception:
|
||||
pass
|
||||
return _gateway_pids.get(name)
|
||||
|
||||
|
||||
def _is_gateway_running(name: str) -> bool:
|
||||
"""Check if a gateway process is running."""
|
||||
pid = _get_gateway_pid(name)
|
||||
if pid:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _get_gateway_info(name: str) -> str:
|
||||
"""Get additional info about a gateway."""
|
||||
pid = _get_gateway_pid(name)
|
||||
if pid:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ps", "-p", str(pid), "-o", "etime=", "-o", "args="],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
parts = result.stdout.strip().split(None, 1)
|
||||
if len(parts) >= 2:
|
||||
elapsed = parts[0]
|
||||
return f"PID {pid} · running {elapsed}"
|
||||
except Exception:
|
||||
pass
|
||||
return f"PID {pid}"
|
||||
return ""
|
||||
|
||||
|
||||
def _detect_telegram_gateway() -> dict:
|
||||
"""Detect if Telegram gateway is configured and running."""
|
||||
hermes_home = _get_hermes_home()
|
||||
gateway_running = False
|
||||
info = ""
|
||||
|
||||
# Check if there's a telegram gateway config
|
||||
config_paths = [
|
||||
hermes_home / "gateways" / "telegram",
|
||||
hermes_home / "gateway" / "telegram",
|
||||
hermes_home / ".env",
|
||||
]
|
||||
|
||||
has_config = False
|
||||
for p in config_paths:
|
||||
if p.exists():
|
||||
has_config = True
|
||||
break
|
||||
|
||||
if has_config:
|
||||
gateway_running = _is_gateway_running("telegram")
|
||||
if gateway_running:
|
||||
info = _get_gateway_info("telegram")
|
||||
|
||||
return {
|
||||
"name": "telegram",
|
||||
"type": "telegram",
|
||||
"running": gateway_running,
|
||||
"info": info,
|
||||
"has_config": has_config,
|
||||
}
|
||||
|
||||
|
||||
def _detect_openclaw_gateway() -> dict:
|
||||
"""Detect if OpenClaw gateway is configured and running."""
|
||||
hermes_home = _get_hermes_home()
|
||||
gateway_running = False
|
||||
info = ""
|
||||
|
||||
config_paths = [
|
||||
hermes_home / "gateways" / "openclaw",
|
||||
hermes_home / "gateway" / "openclaw",
|
||||
]
|
||||
|
||||
has_config = False
|
||||
for p in config_paths:
|
||||
if p.exists():
|
||||
has_config = True
|
||||
break
|
||||
|
||||
if has_config:
|
||||
gateway_running = _is_gateway_running("openclaw")
|
||||
if gateway_running:
|
||||
info = _get_gateway_info("openclaw")
|
||||
|
||||
return {
|
||||
"name": "openclaw",
|
||||
"type": "openclaw",
|
||||
"running": gateway_running,
|
||||
"info": info,
|
||||
"has_config": has_config,
|
||||
}
|
||||
|
||||
|
||||
def _discover_gateways() -> list[dict]:
|
||||
"""Discover all available and configured gateways."""
|
||||
gateways = []
|
||||
|
||||
# Always show telegram if detected
|
||||
telegram = _detect_telegram_gateway()
|
||||
gateways.append(telegram)
|
||||
|
||||
# Check for openclaw
|
||||
openclaw = _detect_openclaw_gateway()
|
||||
gateways.append(openclaw)
|
||||
|
||||
# Add any manually registered gateways
|
||||
with _gateways_lock:
|
||||
for name, info in _gateways.items():
|
||||
if not any(g["name"] == name for g in gateways):
|
||||
running = _is_gateway_running(name)
|
||||
gw_info = _get_gateway_info(name) if running else ""
|
||||
gateways.append({
|
||||
"name": name,
|
||||
"type": info.get("type", "unknown"),
|
||||
"running": running,
|
||||
"info": gw_info,
|
||||
"has_config": True,
|
||||
})
|
||||
|
||||
return gateways
|
||||
|
||||
|
||||
def list_gateways_api() -> list[dict]:
|
||||
"""List all gateways with their status."""
|
||||
return _discover_gateways()
|
||||
|
||||
|
||||
def start_gateway_api(name: str) -> dict:
|
||||
"""Start a gateway by name."""
|
||||
# Check if already running
|
||||
if _is_gateway_running(name):
|
||||
raise RuntimeError(f"Gateway '{name}' is already running")
|
||||
|
||||
hermes_home = _get_hermes_home()
|
||||
|
||||
# Determine the gateway type and command
|
||||
if name == "telegram":
|
||||
cmd = ["hermes", "gateway", "run", "--type", "telegram"]
|
||||
elif name == "openclaw":
|
||||
cmd = ["hermes", "gateway", "run", "--type", "openclaw"]
|
||||
else:
|
||||
cmd = ["hermes", "gateway", "run", "--name", name]
|
||||
|
||||
# Start the gateway process
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=str(hermes_home),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
start_new_session=True
|
||||
)
|
||||
_gateway_pids[name] = proc.pid
|
||||
|
||||
# Give it a moment to start
|
||||
time.sleep(1)
|
||||
|
||||
if proc.poll() is not None:
|
||||
# Process already terminated
|
||||
stdout, stderr = proc.communicate()
|
||||
raise RuntimeError(f"Gateway failed to start: {stderr.decode()[:200]}")
|
||||
|
||||
return {"ok": True, "message": f"Gateway '{name}' started", "pid": proc.pid}
|
||||
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError("hermes CLI not found in PATH. Is Hermes installed?")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to start gateway: {e}")
|
||||
|
||||
|
||||
def stop_gateway_api(name: str) -> dict:
|
||||
"""Stop a gateway by name."""
|
||||
pid = _get_gateway_pid(name)
|
||||
|
||||
if not pid:
|
||||
raise RuntimeError(f"Gateway '{name}' is not running")
|
||||
|
||||
try:
|
||||
os.kill(pid, 9) # SIGKILL
|
||||
time.sleep(0.5)
|
||||
if name in _gateway_pids:
|
||||
del _gateway_pids[name]
|
||||
return {"ok": True, "message": f"Gateway '{name}' stopped"}
|
||||
except OSError as e:
|
||||
raise RuntimeError(f"Failed to stop gateway: {e}")
|
||||
|
||||
|
||||
def restart_gateway_api(name: str) -> dict:
|
||||
"""Restart a gateway by name."""
|
||||
# Check if running first
|
||||
if not _is_gateway_running(name):
|
||||
raise RuntimeError(f"Gateway '{name}' is not running")
|
||||
|
||||
# Stop it
|
||||
stop_gateway_api(name)
|
||||
time.sleep(1)
|
||||
|
||||
# Start it again
|
||||
return start_gateway_api(name)
|
||||
|
||||
|
||||
def add_gateway_api(name: str, gw_type: str = "telegram") -> dict:
|
||||
"""Register a new gateway."""
|
||||
# Validate name
|
||||
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$", name):
|
||||
raise ValueError("Invalid gateway name")
|
||||
|
||||
# Check if already exists
|
||||
for g in _discover_gateways():
|
||||
if g["name"] == name:
|
||||
raise FileExistsError(f"Gateway '{name}' already exists")
|
||||
|
||||
with _gateways_lock:
|
||||
_gateways[name] = {"type": gw_type, "registered_at": time.time()}
|
||||
|
||||
return {"ok": True, "message": f"Gateway '{name}' added as {gw_type}"}
|
||||
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()),
|
||||
}
|
||||
@@ -54,6 +54,7 @@ from api.helpers import (
|
||||
_redact_text,
|
||||
)
|
||||
from api import mc as _mc
|
||||
from api import agents as _agents
|
||||
|
||||
# ── CSRF: validate Origin/Referer on POST ────────────────────────────────────
|
||||
import re as _re
|
||||
@@ -770,6 +771,18 @@ def handle_get(handler, parsed) -> bool:
|
||||
limit = int(parse_qs(parsed.query).get("limit", ["50"])[0])
|
||||
return j(handler, {"feed": _mc.get_feed(limit=limit)})
|
||||
|
||||
# ── Agents API (Rose + Tier-2) ──
|
||||
if parsed.path == "/api/agents":
|
||||
return j(handler, _agents.list_agents())
|
||||
|
||||
if parsed.path.startswith("/api/agents/inbox/"):
|
||||
agent_id = parsed.path.split("/")[-1]
|
||||
return j(handler, _agents.get_agent_inbox(agent_id))
|
||||
|
||||
if parsed.path.startswith("/api/agents/config/"):
|
||||
agent_id = parsed.path.split("/")[-1]
|
||||
return j(handler, _agents.get_agent_config(agent_id))
|
||||
|
||||
# ── Profile API (GET) ──
|
||||
if parsed.path == "/api/profiles":
|
||||
from api.profiles import list_profiles_api, get_active_profile_name
|
||||
|
||||
Reference in New Issue
Block a user