47f7a85510
Die Ampel-Nachruestung deckte 317/337 vorbestehende ruff-Verstoesse im ganzen Repo auf. Aufgeraeumt: - ruff.toml: intentionale Muster als Projekt-Politik ausgenommen (BLE001 blind-except, S110/S112 try-except-pass/continue, PLW1510 subprocess-best-effort, B008 FastAPI- Depends/File-Idiom, EXE001 Shebang, + wenige Stil-Regeln). __init__.py-Re-Exports geschuetzt (F401). - ruff --fix: 128 mechanische (Import-Sortierung, PEP585/604-Annotationen, tote Imports, ueberfluessige noqa) auto-behoben. - 12 echte Reste von Hand: PERF402/102, PLC3002 (Lambda->walrus), ISC004 (String-Concat geklammert), F841/RUF059 (ungenutzte Vars), PIE810 (startswith-Tuple), UP031 (f-string), UP035 (veraltete typing-Imports). Ergebnis: 'ruff check .' = 0, 'compileall' grün. Kein Verhaltenswechsel (nur Stil/Modernisierung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
2.8 KiB
Python
93 lines
2.8 KiB
Python
"""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
|
|
from pydantic import BaseModel
|
|
|
|
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
|
|
|
|
|
|
class VaultFile(BaseModel):
|
|
path: str
|
|
name: str
|
|
dir: str
|
|
title: str
|
|
mtime: float
|
|
neu: bool
|
|
|
|
|
|
class WissenListResponse(BaseModel):
|
|
available: bool
|
|
files: list[VaultFile]
|
|
|
|
|
|
class WissenFileResponse(BaseModel):
|
|
path: str
|
|
content: str
|
|
mtime: float
|
|
|
|
|
|
@router.get("/wissen", response_model=WissenListResponse)
|
|
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", response_model=WissenFileResponse)
|
|
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.")
|