import os import markdown from fastapi import APIRouter, Request, HTTPException, Depends from fastapi.responses import HTMLResponse from deps import current_user router = APIRouter() # Whitelist allowed files. # We look for them in the root directory relative to 'app' parent. # Since app is in /app inside docker, the root volume mount is usually at /app or mapped. # Code Tour Step 828 shows 'app/main.py' path analysis. # If workdir is /app (docker), and files are in /app (root of repo mapped to /app), then "README.md" is at "./README.md". ALLOWED_DOCS = { "README.md": "Start Guide (Readme)", "QUICKSTART.md": "Schnellstart", "HANDBUCH.md": "Benutzerhandbuch", "CODE_TOUR.md": "Code Tour (Dev)", "ARCHITECTURE_ROLES_GROUPS.md": "Architektur", "DIARY_RAG_README.md": "Tagebuch & RAG Info", "CrumbTech.md": "Technische Details", "QDRANT_ACCESS.md": "Vektor DB Access", "docs_git.md": "Guide - Git & Versionierung", "deploy_security_fixes.sh": "Security Script (Source)" # Maybe viewing scripts is cool too? Let's stick to MD for now. } from config import get_settings @router.get("/docs", response_class=HTMLResponse) async def list_docs(req: Request): """ List available documentation files dynamically. """ base_path = _get_docs_base_path() # 1. Pinned Docs (from whitelist) pinned = [] pinned_paths = set() for filename, title in ALLOWED_DOCS.items(): # Check root level only for pinned items (simpler, or check recursively and pin first?) # Legacy behavior was pinned items are usually in root. # Let's verify existence. full_path = _find_file_recursive(base_path, filename) if full_path: # We want the relative path as the ID rel_path = os.path.relpath(full_path, base_path) pinned.append({"name": title, "file": rel_path}) pinned_paths.add(rel_path) # 2. All other docs (dynamic scan) library = [] if os.path.exists(base_path): for root, dirs, files in os.walk(base_path): for file in files: if file.endswith(".md"): full_path = os.path.join(root, file) rel_path = os.path.relpath(full_path, base_path) if rel_path not in pinned_paths: # Improved Naming: File name + Parent Dir if nested # e.g. crumbcodex/README.md -> "Crumbcodex / Readme" parent = os.path.basename(root) safe_name = file.replace(".md", "").replace("-", " ").title() if parent and parent != os.path.basename(base_path) and parent != "docs": # If deeper than root display_name = f"{parent.title()} / {safe_name}" else: display_name = safe_name library.append({"name": display_name, "file": rel_path}) # Combine all_docs = pinned + sorted(library, key=lambda x: x["name"]) return req.app.state.render( req, "pages/docs_index.html", docs=all_docs, page_title="Dokumentation" ) @router.get("/docs/{file_path:path}", response_class=HTMLResponse) async def view_doc(req: Request, file_path: str): """ Render a specific markdown file. specific via relative path. """ # Security: Prevent traversal up if ".." in file_path: raise HTTPException(400, "Invalid path.") base_path = _get_docs_base_path() full_path = os.path.join(base_path, file_path) if not os.path.exists(full_path) or not os.path.isfile(full_path): # Fallback: maybe it was a legacy link with just filename? # Try finding it recursively if direct path fails found = _find_file_recursive(base_path, os.path.basename(file_path)) if found: full_path = found else: raise HTTPException(404, "File not on server.") try: with open(full_path, "r", encoding="utf-8") as f: content = f.read() html_content = markdown.markdown( content, extensions=['tables', 'fenced_code', 'nl2br'] ) # Determine Title filename = os.path.basename(full_path) # Check if pinned to get nice title title = filename.replace(".md", "").replace("-", " ").title() for k, v in ALLOWED_DOCS.items(): if k == filename: title = v break return req.app.state.render( req, "pages/doc_viewer.html", doc_title=title, doc_content=html_content, filename=file_path # Keep full path for context? Or filename? Template uses it for back link maybe? ) except Exception as e: raise HTTPException(500, f"Error rendering document: {e}") def _get_docs_base_path(): try: settings = get_settings() base_path = settings.docs_path except: base_path = "docs" if not os.path.isabs(base_path) and not os.path.exists(base_path): if os.path.exists("/docs_root"): base_path = "/docs_root" else: base_path = "." return base_path def _find_file(base_path, filename): """Legacy helper: find in base or level 1 subdirs.""" return _find_file_recursive(base_path, filename) def _find_file_recursive(base_path, filename): """Find file recursively in base_path.""" # 1. Direct check direct = os.path.join(base_path, filename) if os.path.exists(direct) and os.path.isfile(direct): return direct # 2. Walk for root, dirs, files in os.walk(base_path): if filename in files: return os.path.join(root, filename) return None