From 6a9b42850a7ca7c07e9c968b2d9cbb09361686b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=BCmel=20Branko?= Date: Thu, 25 Dec 2025 23:15:27 +0100 Subject: [PATCH] Enable dynamic docs discovery --- app/routers/docs_reader.py | 85 +++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 24 deletions(-) diff --git a/app/routers/docs_reader.py b/app/routers/docs_reader.py index 3507638..8a3a747 100644 --- a/app/routers/docs_reader.py +++ b/app/routers/docs_reader.py @@ -32,34 +32,67 @@ from config import get_settings @router.get("/docs", response_class=HTMLResponse) async def list_docs(req: Request): """ - List available documentation files. + List available documentation files dynamically. """ - available = [] base_path = _get_docs_base_path() - for filename, title in ALLOWED_DOCS.items(): - # Check root and subdirectories (1 level deep) - full_path = _find_file(base_path, filename) - if full_path: - available.append({"name": title, "file": filename}) + # 1. Pinned Docs (from whitelist) + pinned = [] + # 2. All other docs (dynamic scan) + library = [] + + # helper to check if already in pinned + pinned_filenames = set() + + for filename, title in ALLOWED_DOCS.items(): + full_path = _find_file(base_path, filename) # Helper now needs to look deeper? + if full_path: + pinned.append({"name": title, "file": filename}) + pinned_filenames.add(filename) + + # Scan for all .md files recursively + if os.path.exists(base_path): + for root, dirs, files in os.walk(base_path): + for file in files: + if file.endswith(".md"): + # Calculate relative path or just filename. + # If we use just filename, distinct files with same name in diff folders conflict. + # Current view_doc takes {filename}. Let's assume filenames are unique enough or we handle paths. + # For now, let's just show them if they aren't pinned. + + if file not in pinned_filenames: + # Try to get a nice name (Title from first line?) or filename + name = file.replace(".md", "").replace("-", " ").title() + library.append({"name": name, "file": file}) + + # Combine or separate? Let's just return all in 'docs' for now to fit the template expectation. + # The template iterates 'docs'. + + all_docs = pinned + sorted(library, key=lambda x: x["name"]) + return req.app.state.render( req, "pages/docs_index.html", - docs=available, + docs=all_docs, page_title="Dokumentation" ) @router.get("/docs/{filename}", response_class=HTMLResponse) async def view_doc(req: Request, filename: str): """ - Render a specific markdown file. + Render a specific markdown file. Now supports any MD file found. """ - if filename not in ALLOWED_DOCS: - raise HTTPException(404, "File not found or not allowed.") + # Security: basic check to prevent directory traversal outside allowable scope + if ".." in filename or "/" in filename: # filename param is usually just the last part if caught by path param? + # Actually FastAPI path param "{filename}" stops at slashes unless defined as "{path:path}". + # So 'filename' here is just the leaf name. + pass base_path = _get_docs_base_path() - file_path = _find_file(base_path, filename) + + # Use recursive find + file_path = _find_file_recursive(base_path, filename) if not file_path: raise HTTPException(404, "File not on server.") @@ -68,16 +101,18 @@ async def view_doc(req: Request, filename: str): with open(file_path, "r", encoding="utf-8") as f: content = f.read() - # Convert Markdown to HTML html_content = markdown.markdown( content, extensions=['tables', 'fenced_code', 'nl2br'] ) + # Determine Title + title = ALLOWED_DOCS.get(filename, filename.replace(".md", "").replace("-", " ").title()) + return req.app.state.render( req, "pages/doc_viewer.html", - doc_title=ALLOWED_DOCS[filename], + doc_title=title, doc_content=html_content, filename=filename ) @@ -100,17 +135,19 @@ def _get_docs_base_path(): return base_path def _find_file(base_path, filename): - """Find file in base_path or immediate subdirectories.""" - # 1. Direct match + """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): + if os.path.exists(direct) and os.path.isfile(direct): return direct - # 2. Check subdirectories (max depth 1) - if os.path.isdir(base_path): - for entry in os.scandir(base_path): - if entry.is_dir(): - sub_path = os.path.join(entry.path, filename) - if os.path.exists(sub_path): - return sub_path + # 2. Walk + for root, dirs, files in os.walk(base_path): + if filename in files: + return os.path.join(root, filename) + return None