feat: Editor laedt vorhandene Dateien + manueller Code-Toggle (App + Diagnostic)

Editor zeigte "keine Datei", obwohl ARIA schon Dateien geschrieben hatte — er las
NUR den Live-code_file-Stream, nie den Bestand. Jetzt:
- Brain: GET /projects/<id>/files + /file (liest /shared/projects/<id>/, pfad-sicher,
  512KB-Cap). kind in ProjectUpdateBody (PATCH akzeptiert 'code'|'chat').
- App: brainApi.listProjectFiles/readProjectFile/setProjectKind. CodeEditorTile
  holt beim Oeffnen die vorhandene Dateiliste + laedt Inhalt (Live-Version hat
  Vorrang). ProjectsBrowser-Edit: Code-Projekt-Toggle (spiegelt sofort in
  projectFocus → Cockpit-Panels).
- Diagnostic: </> Code-Toggle je Projektzeile + Code-Badge.

py/node/tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:20:39 +02:00
co-authored by Claude Opus 4.8
parent 9dd7a2cb14
commit 5fa5d79ad8
5 changed files with 182 additions and 30 deletions
+56
View File
@@ -827,17 +827,73 @@ class ProjectUpdateBody(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
hidden: Optional[bool] = None
kind: Optional[str] = None # 'code' | 'chat' — manuell setzbar (App/Diagnostic)
@app.patch("/projects/{project_id}")
def projects_update(project_id: str, body: ProjectUpdateBody):
patch = body.dict(exclude_unset=True)
if "kind" in patch and patch["kind"] not in ("code", "chat", None):
raise HTTPException(status_code=400, detail="kind muss 'code' oder 'chat' sein")
p = projects_mod.update_project(project_id, patch)
if p is None:
raise HTTPException(status_code=404, detail=f"Projekt {project_id} nicht gefunden")
return p
# ── Code-Dateien eines Projekts (/shared/projects/<pid>/) ───────────
# Der Live-Editor streamt ARIAs Writes; diese Endpoints liefern zusaetzlich die
# BEREITS vorhandenen Dateien, damit der Editor beim Oeffnen nicht leer ist.
_PROJECT_FILES_ROOT = "/shared/projects"
_PROJECT_FILE_MAX = 512 * 1024
def _project_dir(project_id: str) -> str:
base = os.path.realpath(os.path.join(_PROJECT_FILES_ROOT, project_id or ""))
root = os.path.realpath(_PROJECT_FILES_ROOT)
if base != root and not base.startswith(root + os.sep):
raise HTTPException(status_code=400, detail="ungueltige project_id")
return base
@app.get("/projects/{project_id}/files")
def project_files(project_id: str):
base = _project_dir(project_id)
out = []
if os.path.isdir(base):
for dirpath, dirs, files in os.walk(base):
dirs[:] = [d for d in dirs if d not in
(".git", "node_modules", "__pycache__", ".venv", "venv")]
for f in files:
full = os.path.join(dirpath, f)
rel = os.path.relpath(full, base).replace("\\", "/")
try:
sz = os.path.getsize(full)
except OSError:
sz = 0
out.append({"path": rel, "size": sz})
out.sort(key=lambda x: x["path"])
return {"projectId": project_id, "files": out}
@app.get("/projects/{project_id}/file")
def project_file(project_id: str, path: str):
base = _project_dir(project_id)
target = os.path.realpath(os.path.join(base, path))
if target != base and not target.startswith(base + os.sep):
raise HTTPException(status_code=400, detail="Pfad ausserhalb des Projekts")
if not os.path.isfile(target):
raise HTTPException(status_code=404, detail="Datei nicht gefunden")
if os.path.getsize(target) > _PROJECT_FILE_MAX:
raise HTTPException(status_code=413, detail="Datei zu gross fuer den Editor")
try:
with open(target, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))
return {"projectId": project_id, "path": path, "content": content}
@app.get("/conversation/stats")
def conversation_stats():
return conversation().stats()