feat: Projects tab, 4-tab dashboard, full knowledge browser
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
This commit is contained in:
Henrik Jess Nielsen
2026-05-09 16:54:30 +02:00
parent 70418bc45b
commit fa4534bfb2
6 changed files with 829 additions and 769 deletions

61
app/routers/projects.py Normal file
View File

@@ -0,0 +1,61 @@
"""Projects router — registered git repos + work contexts from Redis."""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter
from app.redis_client import get_redis, list_projects, list_workcontexts, PROJECT_KEY_PREFIX, WORKCONTEXT_KEY_PREFIX
router = APIRouter(prefix="/api/v1", tags=["projects"])
@router.get("/projects")
def api_list_projects(context: Optional[str] = None, q: Optional[str] = None):
r = get_redis()
projects = list_projects(r, context=context)
# Merge in work contexts (keyed by path)
wc_keys = r.keys(f"{WORKCONTEXT_KEY_PREFIX}*")
import json
work_contexts: dict = {}
for key in wc_keys:
raw = r.get(key)
if raw:
try:
path = key[len(WORKCONTEXT_KEY_PREFIX):]
work_contexts[path] = json.loads(raw)
except Exception:
pass
# Attach work context and apply search filter
result = []
for p in projects:
path = p.get("path", "")
p["work_context"] = work_contexts.get(path)
if q and q.lower() not in (p.get("name", "") + p.get("path", "") + " ".join(p.get("tags", []))).lower():
continue
result.append(p)
# Sort: projects with recent work context first
result.sort(
key=lambda p: (
p["work_context"]["saved_at"] if p.get("work_context") else "",
),
reverse=True,
)
# Context breakdown
contexts = {}
for p in result:
c = p.get("context", "unknown")
contexts[c] = contexts.get(c, 0) + 1
return {"projects": result, "total": len(result), "contexts": contexts}
@router.get("/workcontexts")
def api_list_workcontexts():
r = get_redis()
return {"contexts": list_workcontexts(r)}