update
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user