Auftragsbuch/Chronik/Wissen/Zeitmaschine: Backend + Annahme-Runner (Ein-Klick-Gate)
Der letzte Meter des propose-only-Kreislaufs: Vorschlags-Branches (Werkstatt/ Orchestrator) und Traum-Skill-Kandidaten werden als API sichtbar; Annehmen laeuft als detached systemd-Unit (Merge im Worktree -> Push main -> Deploy -> Health -> Auto-Revert bei Rot). Dazu: Chronik-Endpoint (Announce-Store als Timeline), Wissens-Vault-Reader (read-only, Traversal-Guard), Zeitmaschine (Snapshots + detached Restore) und Morgenlage-Spiegel im Chef-Gutachter-Feed (priority=silent). python-multipart explizit in requirements (voice braucht es, war implizit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""Wissens-Vault-Reader: die nächtlichen Traum-Notizen (~/wissens-vault) als klickbares
|
||||
Wiki in der Zentrale. Bewusst READ-ONLY — geschrieben wird der Vault nur vom Traum-Cron
|
||||
(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
VAULT = Path(os.environ.get("MC_VAULT_DIR", "~/wissens-vault")).expanduser()
|
||||
|
||||
|
||||
def _available() -> bool:
|
||||
return VAULT.is_dir()
|
||||
|
||||
|
||||
def _title_of(p: Path) -> str:
|
||||
"""Erste nicht-leere Zeile (ohne Markdown-#) als Anzeigename."""
|
||||
try:
|
||||
with p.open(encoding="utf-8", errors="replace") as fh:
|
||||
for line in fh:
|
||||
s = line.strip()
|
||||
if s:
|
||||
return s.lstrip("# ").strip()[:160]
|
||||
except OSError:
|
||||
pass
|
||||
return p.stem
|
||||
|
||||
|
||||
@router.get("/wissen")
|
||||
def list_vault() -> dict:
|
||||
"""Alle Markdown-Notizen des Vaults (relativer Pfad, Titel, Ordner, Alter)."""
|
||||
if not _available():
|
||||
return {"available": False, "files": []}
|
||||
files = []
|
||||
now = time.time()
|
||||
for p in sorted(VAULT.rglob("*.md")):
|
||||
if ".git" in p.parts:
|
||||
continue
|
||||
rel = p.relative_to(VAULT).as_posix()
|
||||
st = p.stat()
|
||||
files.append({
|
||||
"path": rel,
|
||||
"name": p.stem,
|
||||
"dir": p.parent.relative_to(VAULT).as_posix() if p.parent != VAULT else "",
|
||||
"title": _title_of(p),
|
||||
"mtime": st.st_mtime,
|
||||
"neu": (now - st.st_mtime) < 36 * 3600, # „neu seit gestern Nacht"
|
||||
})
|
||||
files.sort(key=lambda f: f["mtime"], reverse=True)
|
||||
return {"available": True, "files": files}
|
||||
|
||||
|
||||
@router.get("/wissen/datei")
|
||||
def read_file(pfad: str) -> dict:
|
||||
"""Inhalt einer Vault-Notiz — Traversal hart geblockt (resolve + is_relative_to)."""
|
||||
if not _available():
|
||||
raise HTTPException(404, "Wissens-Vault liegt auf der Box (hier nicht verfügbar).")
|
||||
try:
|
||||
target = (VAULT / pfad).resolve()
|
||||
if not target.is_relative_to(VAULT.resolve()) or target.suffix != ".md" or not target.is_file():
|
||||
raise HTTPException(404, "Notiz nicht gefunden.")
|
||||
return {"path": pfad, "content": target.read_text(encoding="utf-8", errors="replace")[:400_000],
|
||||
"mtime": target.stat().st_mtime}
|
||||
except HTTPException:
|
||||
raise
|
||||
except (ValueError, OSError):
|
||||
raise HTTPException(404, "Notiz nicht lesbar.")
|
||||
Reference in New Issue
Block a user