docs read fix

This commit is contained in:
JiriUhlir
2026-08-03 12:04:13 +02:00
parent eb7ee612c3
commit d50093e893
3 changed files with 69 additions and 15 deletions
+43 -13
View File
@@ -32,6 +32,13 @@ MAX_DOC_FILES = 100
MAX_DOC_BYTES = 300_000
REQUEST_TIMEOUT = 8
# Gitea si u stromu repozitáře vynucuje vlastní stránkování (api.DEFAULT_PAGING_NUM, ve
# výchozím nastavení 30 položek) a náš per_page si klidně sníží. Strom vrací VŠECHNY soubory,
# ne jen *.md, takže bez dotažení dalších stránek by se dokumentace ve větším repu vůbec
# nenašla. Proto čteme stránku po stránce, dokud Gitea hlásí truncated.
TREE_PAGE_SIZE = 100
MAX_TREE_PAGES = 50
class RepoDocsError(RuntimeError):
"""Dokumentaci se nepodařilo načíst (chybí token, Gitea je nedostupná, repo neexistuje)."""
@@ -65,16 +72,31 @@ def list_markdown_files(repo: str, branch: str = "") -> list[dict]:
raise RepoDocsError("Služba nemá evidovaný název repozitáře.")
ref = (branch or "").strip() or _default_branch(repo)
payload = _request_json(f"/api/v1/repos/{quote(gitea_org())}/{quote(repo)}/git/trees/{quote(ref, safe='')}?recursive=true&per_page=1000")
tree_url = f"/api/v1/repos/{quote(gitea_org())}/{quote(repo)}/git/trees/{quote(ref, safe='')}"
files = []
for entry in payload.get("tree") or []:
if entry.get("type") != "blob":
continue
path = entry.get("path") or ""
if not is_markdown_path(path):
continue
files.append({"path": path, "size": int(entry.get("size") or 0)})
seen_entries = 0
for page in range(1, MAX_TREE_PAGES + 1):
payload = _request_json(f"{tree_url}?recursive=true&per_page={TREE_PAGE_SIZE}&page={page}")
entries = payload.get("tree") or []
if not entries:
break
seen_entries += len(entries)
for entry in entries:
if entry.get("type") != "blob":
continue
path = entry.get("path") or ""
if not is_markdown_path(path):
continue
files.append({"path": path, "size": int(entry.get("size") or 0)})
total_count = int(payload.get("total_count") or 0)
if not payload.get("truncated") or (total_count and seen_entries >= total_count):
break
if len(files) >= MAX_DOC_FILES:
logger.info("Repozitář %s má více než %s souborů *.md, seznam je zkrácený.", repo, MAX_DOC_FILES)
break
files.sort(key=lambda item: (item["path"].count("/"), item["path"].lower()))
return files[:MAX_DOC_FILES]
@@ -105,19 +127,23 @@ def read_markdown(repo: str, path: str, branch: str = "") -> str:
def render_markdown(text: str, repo: str) -> str:
"""Vyrenderuje markdown do HTML přes Gitea API.
Portál nemá markdown knihovnu a Gitea render umí a zároveň si výstupní HTML sanitizuje.
Když render selže, vrátíme obsah jako neformátovaný (escapovaný) text - náhled tak
funguje i při výpadku render endpointu.
Portál nemá markdown knihovnu, Gitea render umí a zároveň si výstupní HTML sanitizuje.
Náhled ale nesmí skončit prázdný: když render selže NEBO vrátí prázdné tělo (Gitea to
dělá i bez chybové hlášky, např. když si nepřečte pole text), zobrazíme obsah souboru
jako neformátovaný escapovaný text.
"""
if not text.strip():
return '<p class="muted">Soubor je prázdný.</p>'
payload = json.dumps(
{
"text": text,
"mode": "gfm",
"context": f"/{gitea_org()}/{repo}",
"wiki": False,
}
).encode("utf-8")
rendered = ""
try:
rendered = _request_bytes(
"/api/v1/markdown",
@@ -126,11 +152,15 @@ def render_markdown(text: str, repo: str) -> str:
accept="text/html",
content_type="application/json",
).decode("utf-8", errors="replace")
return f'<div class="md-body">{rendered}</div>'
except RepoDocsError as exc:
logger.warning("Gitea markdown render selhal (%s), zobrazuji neformátovaný text: %s", repo, exc)
if not rendered.strip():
logger.warning("Gitea markdown render vrátil prázdný výstup (%s), zobrazuji neformátovaný text.", repo)
return f'<pre class="md-raw">{html.escape(text)}</pre>'
return f'<div class="md-body">{rendered}</div>'
def _default_branch(repo: str) -> str:
"""Výchozí větev repozitáře podle Gitea (služby zakládané portálem mají main)."""
+4
View File
@@ -177,6 +177,10 @@ function loadLazyPanel(panel) {
return response.text();
})
.then((text) => {
// Prázdná odpověď by panel jen vyprázdnila a vypadalo by to jako zaseknuté načítání.
if (!text.trim()) {
throw new Error("server vrátil prázdnou odpověď");
}
panel.innerHTML = text;
})
.catch((error) => {
+22 -2
View File
@@ -1,10 +1,30 @@
import html
import unicodedata
from pathlib import Path
from ..config import PORTAL_PREFIX
APP_NAME = "CSBot Services Portal"
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
def _asset_version(filename: str) -> str:
"""Otisk statického souboru do URL (?v=...).
Starlette u statiky neposílá Cache-Control, takže prohlížeč umí po nasazení držet starý
styles.css/portal.js a nová stránka pak vypadá rozbitě. Otisk se počítá při startu
aplikace - po redeployi kontejneru se změní a prohlížeč si soubor stáhne znovu.
"""
try:
return str(int((STATIC_DIR / filename).stat().st_mtime))
except OSError:
return "0"
STYLES_VERSION = _asset_version("styles.css")
SCRIPT_VERSION = _asset_version("portal.js")
# Sekce viditelné pouze pro tento e-mail (Migration Readiness, Runtime Management, Environment).
SUPER_ADMIN_EMAIL = "jiri.uhlir59@gmail.com"
@@ -104,8 +124,8 @@ def page(title: str, body: str, user=None) -> str:
<title>{APP_NAME} - {html.escape(title)}</title>
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/vendor/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/vendor/fontawesome/css/solid.min.css">
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/styles.css">
<script src="{PORTAL_PREFIX}/static/portal.js"></script>
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/styles.css?v={STYLES_VERSION}">
<script src="{PORTAL_PREFIX}/static/portal.js?v={SCRIPT_VERSION}"></script>
</head>
<body>
<header>