Auftragsbuch: Konzept-Ansicht fuer IDE-Projekte (KONZEPT.md inline)
IDE-Vorbereiten-Karten bekommen einen 'Konzept'-Knopf: MC2 holt KONZEPT.md aus dem vorbereiteten Gitea-Repo (Gitea-Raw-API + Auth aus git-credentials) und rendert es direkt im Auftragsbuch (leichter MD-Renderer lib/markdown.tsx). So liest der Commander das ausgearbeitete Konzept in der GUI, ohne das Repo aufzumachen (User-Wunsch: 'lesen gehoert in MC2, feilen in die IDE'). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,3 +90,8 @@ def get_log(id: str) -> dict:
|
||||
@router.get("/ideen/{id}/ergebnis")
|
||||
def get_ergebnis(id: str) -> dict:
|
||||
return ideen.ergebnis_of(id)
|
||||
|
||||
|
||||
@router.get("/ideen/{id}/konzept")
|
||||
def get_konzept(id: str) -> dict:
|
||||
return ideen.konzept_of(id)
|
||||
|
||||
@@ -558,6 +558,100 @@ def prio_task(task_id: str, richtung: str) -> dict:
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Konzept-Ansicht (IDE-Welt) — KONZEPT.md aus dem vorbereiteten Repo holen ──
|
||||
# Der projektstart-Worker legt ein Gitea-Repo an und schreibt das wasserdichte
|
||||
# Konzept als KONZEPT.md hinein. Hier holen wir es (Gitea-API + Auth aus
|
||||
# ~/.git-credentials), damit der Commander es DIREKT im Auftragsbuch liest,
|
||||
# ohne das Repo aufzumachen (User-Wunsch 20.07.).
|
||||
_GITEA_HOST = "git.tobisniceshomelab.ddnsfree.com"
|
||||
_GITEA_REPO_RX = re.compile(
|
||||
re.escape(_GITEA_HOST) + r"/([A-Za-z0-9_.-]+/[A-Za-z0-9_][A-Za-z0-9_.-]*?)(?:\.git)?(?=[\s)\"'<>]|/?$)"
|
||||
)
|
||||
_REPO_FALLBACK_RX = re.compile(r"[Rr]epo\S*[:\s]+([A-Za-z0-9_.-]+/[A-Za-z0-9_][A-Za-z0-9_.-]*)")
|
||||
_KONZEPT_NAMES = ("KONZEPT.md", "Konzept.md", "konzept.md", "CONCEPT.md")
|
||||
_konzept_cache: dict = {}
|
||||
|
||||
|
||||
def _gitea_creds() -> tuple | None:
|
||||
"""(user, token) fuer den oeffentlichen Gitea-Host aus ~/.git-credentials."""
|
||||
try:
|
||||
cred = Path("~/.git-credentials").expanduser()
|
||||
for line in cred.read_text(encoding="utf-8").splitlines():
|
||||
m = re.match(r"https://([^:]+):([^@]+)@" + re.escape(_GITEA_HOST), line.strip())
|
||||
if m:
|
||||
return m.group(1), m.group(2)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _extract_repo(text: str) -> str | None:
|
||||
"""Ziel-Repo (owner/name) aus dem Ergebnis-/Body-Text der Karte ziehen."""
|
||||
if not text:
|
||||
return None
|
||||
m = _GITEA_REPO_RX.search(text)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = _REPO_FALLBACK_RX.search(text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def konzept_of(task_id: str) -> dict:
|
||||
"""Das KONZEPT.md des vorbereiteten IDE-Projekts holen (aus dem Gitea-Repo)."""
|
||||
if not _available():
|
||||
return {"available": False}
|
||||
if not _TASK_ID_RX.match(task_id or ""):
|
||||
return {"available": True, "error": "Ungültige Aufgaben-Nummer."}
|
||||
if task_id in _konzept_cache:
|
||||
return _konzept_cache[task_id]
|
||||
|
||||
# Repo aus Ergebnis + Body der Karte ziehen (show --json trägt beides).
|
||||
try:
|
||||
r = _hermes(["show", task_id, "--json"], timeout=30)
|
||||
d = _parse_json(r.stdout)
|
||||
except Exception:
|
||||
return {"available": True, "error": "Karte nicht lesbar."}
|
||||
haystack = ""
|
||||
if isinstance(d, dict):
|
||||
haystack = str(d.get("result") or "") + "\n" + str(d.get("body") or "")
|
||||
for ev in d.get("events") or []:
|
||||
if isinstance(ev, dict) and ev.get("kind") == "completed":
|
||||
haystack += "\n" + str((ev.get("payload") or {}).get("summary") or "")
|
||||
repo = _extract_repo(haystack)
|
||||
if not repo:
|
||||
return {"available": True, "repo": None,
|
||||
"error": "Noch kein Repo hinterlegt — das Projekt wird gerade vorbereitet."}
|
||||
|
||||
creds = _gitea_creds()
|
||||
auth = creds if creds else None
|
||||
konzept, used_name = "", None
|
||||
try:
|
||||
import httpx
|
||||
with httpx.Client(timeout=10.0, follow_redirects=True) as c:
|
||||
for name in _KONZEPT_NAMES:
|
||||
url = f"https://{_GITEA_HOST}/{repo}/raw/branch/main/{name}"
|
||||
resp = c.get(url, auth=auth)
|
||||
if resp.status_code == 200 and resp.text.strip():
|
||||
konzept, used_name = resp.text, name
|
||||
break
|
||||
except Exception:
|
||||
log.warning("ideen: KONZEPT-Abruf für %s (%s) fehlgeschlagen", task_id, repo, exc_info=True)
|
||||
return {"available": True, "repo": repo, "error": "Konzept konnte nicht geladen werden."}
|
||||
|
||||
if not konzept:
|
||||
return {"available": True, "repo": repo,
|
||||
"error": "KONZEPT.md im Repo (noch) nicht gefunden."}
|
||||
clone_url = f"https://{_GITEA_HOST}/{repo}.git"
|
||||
data = {"available": True, "repo": repo, "clone_url": clone_url,
|
||||
"datei": used_name, "konzept": konzept[:60000]}
|
||||
# Fertige Konzepte ändern sich selten → cachen (aber nur, wenn die Karte done ist).
|
||||
if isinstance(d, dict) and d.get("status") == "done":
|
||||
if len(_konzept_cache) >= 20:
|
||||
_konzept_cache.pop(next(iter(_konzept_cache)))
|
||||
_konzept_cache[task_id] = data
|
||||
return data
|
||||
|
||||
|
||||
def log_of(task_id: str) -> dict:
|
||||
"""Worker-Log einer Idee (hermes kanban log <id>), ANSI entfernt, letzte ~120 Zeilen."""
|
||||
if not _available():
|
||||
|
||||
Reference in New Issue
Block a user