All checks were successful
Build and Deploy DevOpsDash / build-image (push) Successful in 8s
- Add projects router (Redis projects + workcontexts merged)
- Register projects router in main.py
- Extend knowledge router: howtos, agents, skills via MCP proxy
- Extend mcp_client: list/get howtos, agents, skills
- Rewrite dashboard.html: 4 tabs (Taskz/Worklog/Projects/Knowledge)
- Taskz: sidebar board list + Kanban columns with task modal
- Worklog: context/days picker + standup button
- Projects: context filter sidebar + work context display
- Knowledge: 6 sub-tabs (docs/howtos/agents/skills/adrs/memories)
with markdown rendering via marked.js
151 lines
5.7 KiB
Python
151 lines
5.7 KiB
Python
"""Redis client for DevOpsDash — reads the same keys as DevOpsMCP."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import redis
|
|
|
|
# ── Key prefixes (must match DevOpsMCP exactly) ───────────────────────────────
|
|
BOARD_KEY_PREFIX = "devops-mcp:taskboard:"
|
|
TASK_KEY_PREFIX = "devops-mcp:task:"
|
|
BOARD_INDEX_KEY = "devops-mcp:taskboard:_index"
|
|
|
|
ADR_KEY_PREFIX = "devops-mcp:adr:"
|
|
ADR_INDEX_KEY = "devops-mcp:adr:_index"
|
|
|
|
MEMORY_KEY_PREFIX = "devops-mcp:memory:"
|
|
|
|
PROJECT_KEY_PREFIX = "devops-mcp:projects:"
|
|
PROJECT_INDEX_KEY = "devops-mcp:projects:_index"
|
|
|
|
WORKCONTEXT_KEY_PREFIX = "devops-mcp:workcontext:"
|
|
|
|
|
|
def get_redis() -> redis.Redis:
|
|
url = os.environ.get("REDIS_URL", "redis://localhost:6379")
|
|
return redis.from_url(url, decode_responses=True, socket_connect_timeout=3)
|
|
|
|
|
|
# ── Task boards ───────────────────────────────────────────────────────────────
|
|
|
|
def list_boards(r: redis.Redis, project: Optional[str] = None, status: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
index: List[str] = json.loads(r.get(BOARD_INDEX_KEY) or "[]")
|
|
boards = []
|
|
for bid in index:
|
|
raw = r.get(f"{BOARD_KEY_PREFIX}{bid}")
|
|
if raw:
|
|
board = json.loads(raw)
|
|
if project and project.lower() not in board.get("project", "").lower():
|
|
continue
|
|
if status and board.get("status") != status:
|
|
continue
|
|
boards.append(board)
|
|
boards.sort(key=lambda b: b.get("updated_at", ""), reverse=True)
|
|
return boards
|
|
|
|
|
|
def get_board(r: redis.Redis, board_id: str) -> Optional[Dict[str, Any]]:
|
|
raw = r.get(f"{BOARD_KEY_PREFIX}{board_id}")
|
|
return json.loads(raw) if raw else None
|
|
|
|
|
|
def save_board(r: redis.Redis, board: Dict[str, Any]) -> None:
|
|
bid = board["board_id"]
|
|
r.set(f"{BOARD_KEY_PREFIX}{bid}", json.dumps(board), ex=86400 * 365 * 3)
|
|
index: List[str] = json.loads(r.get(BOARD_INDEX_KEY) or "[]")
|
|
if bid not in index:
|
|
index.append(bid)
|
|
r.set(BOARD_INDEX_KEY, json.dumps(index), ex=86400 * 365 * 3)
|
|
|
|
|
|
def save_task(r: redis.Redis, task: Dict[str, Any]) -> None:
|
|
tid = task["task_id"]
|
|
r.set(f"{TASK_KEY_PREFIX}{tid}", json.dumps(task), ex=86400 * 365 * 3)
|
|
board = get_board(r, task["board_id"])
|
|
if board:
|
|
tasks = board.setdefault("tasks", [])
|
|
existing = next((i for i, t in enumerate(tasks) if t["task_id"] == tid), None)
|
|
if existing is not None:
|
|
tasks[existing] = task
|
|
else:
|
|
tasks.append(task)
|
|
from datetime import datetime, timezone
|
|
board["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
save_board(r, board)
|
|
|
|
|
|
# ── ADRs ──────────────────────────────────────────────────────────────────────
|
|
|
|
def list_adrs(r: redis.Redis, status_filter: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
index: List[str] = json.loads(r.get(ADR_INDEX_KEY) or "[]")
|
|
adrs = []
|
|
for adr_id in index:
|
|
raw = r.get(f"{ADR_KEY_PREFIX}{adr_id}")
|
|
if raw:
|
|
adr = json.loads(raw)
|
|
if status_filter and adr.get("status", "").lower() != status_filter.lower():
|
|
continue
|
|
adrs.append(adr)
|
|
adrs.sort(key=lambda a: a.get("number", 0))
|
|
return adrs
|
|
|
|
|
|
def get_adr(r: redis.Redis, adr_id: str) -> Optional[Dict[str, Any]]:
|
|
raw = r.get(f"{ADR_KEY_PREFIX}{adr_id}")
|
|
return json.loads(raw) if raw else None
|
|
|
|
|
|
# ── Memories ──────────────────────────────────────────────────────────────────
|
|
|
|
def list_memories(r: redis.Redis, entity_type: Optional[str] = None, limit: int = 50) -> List[Dict[str, Any]]:
|
|
keys = r.keys(f"{MEMORY_KEY_PREFIX}*")
|
|
memories = []
|
|
for key in keys:
|
|
if key.endswith("_index"):
|
|
continue
|
|
raw = r.get(key)
|
|
if raw:
|
|
try:
|
|
mem = json.loads(raw)
|
|
if entity_type and mem.get("entity_type", "").lower() != entity_type.lower():
|
|
continue
|
|
memories.append(mem)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
memories.sort(key=lambda m: m.get("created_at", ""), reverse=True)
|
|
return memories[:limit]
|
|
|
|
|
|
# ── Projects ──────────────────────────────────────────────────────────────────
|
|
|
|
def list_projects(r: redis.Redis, context: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
index: List[str] = json.loads(r.get(PROJECT_INDEX_KEY) or "[]")
|
|
projects = []
|
|
for path in index:
|
|
raw = r.get(f"{PROJECT_KEY_PREFIX}{path}")
|
|
if raw:
|
|
proj = json.loads(raw)
|
|
if context and proj.get("context", "").lower() != context.lower():
|
|
continue
|
|
projects.append(proj)
|
|
return projects
|
|
|
|
|
|
def list_workcontexts(r: redis.Redis) -> List[Dict[str, Any]]:
|
|
keys = r.keys(f"{WORKCONTEXT_KEY_PREFIX}*")
|
|
contexts = []
|
|
for key in keys:
|
|
raw = r.get(key)
|
|
if raw:
|
|
try:
|
|
ctx = json.loads(raw)
|
|
ctx["path"] = key[len(WORKCONTEXT_KEY_PREFIX):]
|
|
contexts.append(ctx)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
contexts.sort(key=lambda c: c.get("saved_at", ""), reverse=True)
|
|
return contexts
|