This commit is contained in:
team3
2026-07-03 12:50:32 +02:00
parent 9754cbcfae
commit 91b0d00aa1
27 changed files with 203 additions and 580 deletions

View File

@@ -12,7 +12,6 @@ from config import PROJECTS_DIR, UNI_DIR, PROVIDERS
from database import (
create_guide, delete_guide, get_guide, list_guides,
create_topic, list_topics as db_list_topics, delete_topic,
list_progress, set_progress, delete_progress,
list_block_progress, get_block_progress, set_open_question,
set_block_score_and_streak,
delete_block_data, delete_block_progress, subs_per_level, subs_per_level_raw,
@@ -33,7 +32,7 @@ from models import (
BlocksCreateRequest, BlocksResetStageRequest, BlocksCardRestartRequest, BlocksStatusResponse,
GuideCardResetRequest, GuideFormatRequest,
GuideBoardResetRequest, GuideChatRequest, GuideChatResponse,
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
ProviderInfo,
FolderResponse, BlocksSourceUpdate, BlocksSourceResponse, BlockOverview,
BlockChatRequest, BlockChatResponse,
BlockExamRequest, BlockExamResponse, BlockLearnStateResponse,
@@ -65,19 +64,19 @@ async def get_topics():
@router.get("/stats")
async def get_stats():
"""Tracker: number of topics + per format created/completed."""
guides, progress, levels = await load_learnstate()
guides, levels = await load_learnstate()
topics = set(await db_list_topics()) | {g["topic"] for g in guides} | set(blocks_topics())
if PROJECTS_DIR.is_dir():
topics |= {e.name for e in PROJECTS_DIR.iterdir() if e.is_dir()}
return {"topics": len(topics), "formats": formats_stats(guides, progress, levels)}
return {"topics": len(topics), "formats": formats_stats(guides, levels)}
@router.get("/topics/progress")
async def topic_progress(topic: str):
"""Completion status per format + topic completion — for unlocking the next expansion stage."""
guides, progress, levels = await load_learnstate()
status = {fmt: ist_completed(topic, fmt, guides, progress, levels) for fmt in FORMATE}
status["completed"] = topic_completed(topic, guides, progress, levels)
guides, levels = await load_learnstate()
status = {fmt: ist_completed(topic, fmt, guides, levels) for fmt in FORMATE}
status["completed"] = topic_completed(topic, guides, levels)
return status
@@ -98,29 +97,6 @@ async def remove_topic(topic: str):
return {"ok": True}
def _safe_project_name(name: str) -> str:
if not name or "/" in name or "\\" in name or ".." in name or "\x00" in name:
raise HTTPException(400, "Invalid project name")
return name
@router.get("/projects", response_model=list[ProjectResponse])
async def list_projects():
if not PROJECTS_DIR.is_dir():
return []
return [{"name": entry.name} for entry in sorted(PROJECTS_DIR.iterdir()) if entry.is_dir()]
@router.delete("/projects/{name}")
async def remove_project(name: str):
_safe_project_name(name)
pdir = project_dir(name)
if not pdir.is_dir():
raise HTTPException(404, "Project not found")
shutil.rmtree(pdir)
return {"ok": True}
@router.get("/folders", response_model=list[FolderResponse])
async def list_folders(kind: str):
"""Folders for the source selection: kind=projekt → projects/, kind=uni → uni/."""
@@ -181,12 +157,6 @@ async def get_blocks_board(topic: str):
return snap
@router.get("/blocks/agents")
async def get_blocks_agents(topic: str):
return [{"key": a["key"], "label": a["label"] or a["key"].removeprefix(f"blocks-{topic}-"), "runtime": a["runtime"]}
for a in active_agents(f"blocks-{topic}-")]
@router.post("/blocks/research")
async def add_blocks_research(topic: str, provider: str = "claude"):
"""Attach one more research agent — to the live flow, or attach-or-start."""
@@ -335,24 +305,6 @@ async def get_question_pattern(topic: str, block: str):
return {"pattern": await load_question_pattern_free(topic, block, fe)}
@router.get("/blocks/artefakte")
async def get_artefakte(topic: str, type: str | None = None):
"""Learning artifacts (flashcards/examples) per topic, grouped by block norm — per subblock."""
rows = await get_sub_artefakte(topic, type)
out: dict[str, dict] = {}
for r in rows:
b = out.setdefault(r["block_norm"], {"block": r["block"], "flashcard": [], "example": []})
if r["block"] and not b["block"]:
b["block"] = r["block"]
try:
data = json.loads(r["data"])
except (ValueError, TypeError):
continue
if r["type"] in ("flashcard", "example"):
b[r["type"]].append({"subblock": r["sub_title"], **data})
return {"artefakte": out}
# --- Practice deck: Leitner flashcard pool per topic ---
async def build_practice_deck(topic: str) -> dict:
@@ -627,8 +579,8 @@ async def block_exam_route(req: BlockExamRequest):
@router.post("/guides", response_model=GuideResponse)
async def create(req: GuideCreateRequest):
guides, progress, levels = await load_learnstate()
reason = guide_lock(req.topic.strip(), req.format, guides, progress, levels)
guides, levels = await load_learnstate()
reason = guide_lock(req.topic.strip(), req.format, guides, levels)
if reason:
raise HTTPException(400 if reason == "Erst Blocks erstellen" else 409, reason) # string matches rules.py contract
await create_topic(req.topic.strip())
@@ -653,27 +605,6 @@ async def list_all():
return await list_guides()
@router.get("/guides/locks")
async def guide_locks(topic: str):
"""Lock reasons per format for the ▶ button — None = creatable."""
guides, progress, levels = await load_learnstate()
return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("FullGuide", "Rest", *FORMATE)}
@router.get("/guides/steps")
async def guide_steps(topic: str):
"""Highest fully completed stage index per format (card-based, -1 = none).
Content file present (legacy without cards) → everything done."""
import guide_board
out = {}
for fmt in ("Guide", "FullGuide", "Rest"):
step = await guide_board.done_step(topic, fmt)
if step < 0 and guide_content_path(topic, fmt).exists():
step = len(guide_board.GUIDE_STAGES)
out[fmt] = step
return out
@router.get("/guides/board")
async def get_guide_board(topic: str, format: str = "Guide"):
"""Live guide board: columns with counts + cards (rounds, covered objectives), agents."""
@@ -704,14 +635,6 @@ async def reset_guide_board(req: GuideBoardResetRequest):
return {"ok": True, "moved": moved}
@router.get("/guides/{guide_id}", response_model=GuideResponse)
async def get_one(guide_id: str):
guide = await get_guide(guide_id)
if guide is None:
raise HTTPException(404, "Guide not found")
return guide
@router.get("/guides/{guide_id}/content")
async def guide_content(guide_id: str, level: int = 4):
"""Guide content. `level` (1=A · 2=F · 3=E · 4=V) filters to subblocks up to this
@@ -789,7 +712,6 @@ async def remove_guide_format(req: GuideFormatRequest):
if any(g["status"] in ("queued", "generating") for g in doomed):
return {"ok": True, "status": "generating"}
for g in doomed:
await delete_progress(g["id"])
await delete_guide(g["id"])
await delete_guide_content(req.topic, req.format)
await delete_guide_board(req.topic, req.format)
@@ -805,7 +727,6 @@ async def remove(guide_id: str, slots: bool = False):
guide = await get_guide(guide_id)
if guide is None:
raise HTTPException(404, "Guide not found")
await delete_progress(guide_id)
await delete_guide(guide_id)
# Content/step files are shared by all runs of a topic+format — only delete them
# once no entry needs them anymore. Partial progress (step files without finished
@@ -820,20 +741,3 @@ async def remove(guide_id: str, slots: bool = False):
p.unlink(missing_ok=True)
content.unlink(missing_ok=True)
return {"ok": True}
@router.get("/guides/{guide_id}/progress", response_model=ProgressResponse)
async def get_progress(guide_id: str):
guide = await get_guide(guide_id)
if guide is None:
raise HTTPException(404, "Guide not found")
return {"chapters": await list_progress(guide_id)}
@router.post("/guides/{guide_id}/progress", response_model=ProgressResponse)
async def update_progress(guide_id: str, req: ProgressUpdate):
guide = await get_guide(guide_id)
if guide is None:
raise HTTPException(404, "Guide not found")
await set_progress(guide_id, req.chapter, req.done)
return {"chapters": await list_progress(guide_id)}