docs read fix
This commit is contained in:
+43
-13
@@ -32,6 +32,13 @@ MAX_DOC_FILES = 100
|
|||||||
MAX_DOC_BYTES = 300_000
|
MAX_DOC_BYTES = 300_000
|
||||||
REQUEST_TIMEOUT = 8
|
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):
|
class RepoDocsError(RuntimeError):
|
||||||
"""Dokumentaci se nepodařilo načíst (chybí token, Gitea je nedostupná, repo neexistuje)."""
|
"""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.")
|
raise RepoDocsError("Služba nemá evidovaný název repozitáře.")
|
||||||
|
|
||||||
ref = (branch or "").strip() or _default_branch(repo)
|
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 = []
|
files = []
|
||||||
for entry in payload.get("tree") or []:
|
seen_entries = 0
|
||||||
if entry.get("type") != "blob":
|
for page in range(1, MAX_TREE_PAGES + 1):
|
||||||
continue
|
payload = _request_json(f"{tree_url}?recursive=true&per_page={TREE_PAGE_SIZE}&page={page}")
|
||||||
path = entry.get("path") or ""
|
entries = payload.get("tree") or []
|
||||||
if not is_markdown_path(path):
|
if not entries:
|
||||||
continue
|
break
|
||||||
files.append({"path": path, "size": int(entry.get("size") or 0)})
|
|
||||||
|
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()))
|
files.sort(key=lambda item: (item["path"].count("/"), item["path"].lower()))
|
||||||
return files[:MAX_DOC_FILES]
|
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:
|
def render_markdown(text: str, repo: str) -> str:
|
||||||
"""Vyrenderuje markdown do HTML přes Gitea API.
|
"""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.
|
Portál nemá markdown knihovnu, 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
|
Náhled ale nesmí skončit prázdný: když render selže NEBO vrátí prázdné tělo (Gitea to
|
||||||
funguje i při výpadku render endpointu.
|
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(
|
payload = json.dumps(
|
||||||
{
|
{
|
||||||
"text": text,
|
"text": text,
|
||||||
"mode": "gfm",
|
"mode": "gfm",
|
||||||
"context": f"/{gitea_org()}/{repo}",
|
"context": f"/{gitea_org()}/{repo}",
|
||||||
"wiki": False,
|
|
||||||
}
|
}
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
|
|
||||||
|
rendered = ""
|
||||||
try:
|
try:
|
||||||
rendered = _request_bytes(
|
rendered = _request_bytes(
|
||||||
"/api/v1/markdown",
|
"/api/v1/markdown",
|
||||||
@@ -126,11 +152,15 @@ def render_markdown(text: str, repo: str) -> str:
|
|||||||
accept="text/html",
|
accept="text/html",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
).decode("utf-8", errors="replace")
|
).decode("utf-8", errors="replace")
|
||||||
return f'<div class="md-body">{rendered}</div>'
|
|
||||||
except RepoDocsError as exc:
|
except RepoDocsError as exc:
|
||||||
logger.warning("Gitea markdown render selhal (%s), zobrazuji neformátovaný text: %s", repo, 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'<pre class="md-raw">{html.escape(text)}</pre>'
|
||||||
|
|
||||||
|
return f'<div class="md-body">{rendered}</div>'
|
||||||
|
|
||||||
|
|
||||||
def _default_branch(repo: str) -> str:
|
def _default_branch(repo: str) -> str:
|
||||||
"""Výchozí větev repozitáře podle Gitea (služby zakládané portálem mají main)."""
|
"""Výchozí větev repozitáře podle Gitea (služby zakládané portálem mají main)."""
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ function loadLazyPanel(panel) {
|
|||||||
return response.text();
|
return response.text();
|
||||||
})
|
})
|
||||||
.then((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;
|
panel.innerHTML = text;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
+22
-2
@@ -1,10 +1,30 @@
|
|||||||
import html
|
import html
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from ..config import PORTAL_PREFIX
|
from ..config import PORTAL_PREFIX
|
||||||
|
|
||||||
APP_NAME = "CSBot Services Portal"
|
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).
|
# Sekce viditelné pouze pro tento e-mail (Migration Readiness, Runtime Management, Environment).
|
||||||
SUPER_ADMIN_EMAIL = "jiri.uhlir59@gmail.com"
|
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>
|
<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/fontawesome.min.css">
|
||||||
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/vendor/fontawesome/css/solid.min.css">
|
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/vendor/fontawesome/css/solid.min.css">
|
||||||
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/styles.css">
|
<link rel="stylesheet" href="{PORTAL_PREFIX}/static/styles.css?v={STYLES_VERSION}">
|
||||||
<script src="{PORTAL_PREFIX}/static/portal.js"></script>
|
<script src="{PORTAL_PREFIX}/static/portal.js?v={SCRIPT_VERSION}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
|||||||
Reference in New Issue
Block a user