Реестр MODULE_MOUNTS: API, ui_router, фрагменты главной, EventBus. Главная и страницы модулей с правой навигацией из реестра; wrap_module_html_page. Ingress: публикация alert.received после сохранения в БД. Документация MODULES.md; pytest покрывает API, UI и навигацию. Made-with: Cursor
108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
"""Безопасная сборка HTML с модулей и общий каркас UI (правая колонка меню из реестра)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import html
|
||
import logging
|
||
from collections.abc import Awaitable, Callable
|
||
|
||
from starlette.requests import Request
|
||
|
||
log = logging.getLogger("onguard24.modules.ui")
|
||
|
||
# Общие стили: сетка «контент слева + меню справа», навигация по модулям
|
||
APP_SHELL_CSS = """
|
||
body { font-family: system-ui, sans-serif; margin: 0; background: #fafafa; color: #18181b; }
|
||
a { color: #2563eb; text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
.app-shell { display: flex; flex-direction: row; align-items: flex-start; gap: 1.5rem;
|
||
max-width: 72rem; margin: 0 auto; padding: 1.5rem 1.25rem 2rem; box-sizing: border-box; }
|
||
.app-main { flex: 1; min-width: 0; }
|
||
.app-rail { width: 13.5rem; flex-shrink: 0; position: sticky; top: 1rem;
|
||
background: #fff; border-radius: 8px; box-shadow: 0 1px 3px #0001; padding: 0.75rem 0; }
|
||
.rail-title { margin: 0 0 0.5rem; padding: 0 0.75rem; font-size: 0.75rem; font-weight: 600;
|
||
text-transform: uppercase; letter-spacing: 0.04em; color: #71717a; }
|
||
.rail-nav ul { list-style: none; margin: 0; padding: 0; }
|
||
.rail-item a { display: block; padding: 0.45rem 0.75rem; font-size: 0.9rem; color: #3f3f46; border-radius: 4px; }
|
||
.rail-item a:hover { background: #f4f4f5; }
|
||
.rail-item.is-active a { background: #eff6ff; color: #1d4ed8; font-weight: 600; }
|
||
.module-page-main h1 { margin-top: 0; font-size: 1.35rem; }
|
||
"""
|
||
|
||
|
||
def nav_rail_html(current_slug: str | None = None) -> str:
|
||
"""Правая колонка: пункты из реестра модулей с `ui_router` + «Главная».
|
||
|
||
Новый модуль в `MODULE_MOUNTS` с UI — пункт появляется автоматически.
|
||
"""
|
||
from onguard24.modules.registry import MODULE_MOUNTS
|
||
|
||
home_li = (
|
||
'<li class="rail-item'
|
||
+ (" is-active" if current_slug is None else "")
|
||
+ '"><a href="/">Главная</a></li>'
|
||
)
|
||
items: list[str] = [home_li]
|
||
for m in MODULE_MOUNTS:
|
||
if m.ui_router is None:
|
||
continue
|
||
href = f"/ui/modules/{m.slug}/"
|
||
active = m.slug == current_slug
|
||
licls = "rail-item" + (" is-active" if active else "")
|
||
cur = ' aria-current="page"' if active else ""
|
||
items.append(
|
||
f'<li class="{licls}"><a href="{html.escape(href)}"{cur}>{html.escape(m.title)}</a></li>'
|
||
)
|
||
lis = "".join(items)
|
||
return (
|
||
'<aside class="app-rail" role="navigation" aria-label="Разделы приложения">'
|
||
'<nav class="rail-nav">'
|
||
'<h2 class="rail-title">Разделы</h2>'
|
||
f"<ul>{lis}</ul>"
|
||
"</nav>"
|
||
"</aside>"
|
||
)
|
||
|
||
|
||
def wrap_module_html_page(
|
||
*,
|
||
document_title: str,
|
||
current_slug: str,
|
||
main_inner_html: str,
|
||
) -> str:
|
||
"""Полная HTML-страница модуля: основной контент + то же правое меню, что и на главной."""
|
||
rail = nav_rail_html(current_slug)
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||
<title>{html.escape(document_title)}</title>
|
||
<style>{APP_SHELL_CSS}</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<main class="app-main module-page-main">
|
||
{main_inner_html}
|
||
</main>
|
||
{rail}
|
||
</div>
|
||
</body>
|
||
</html>"""
|
||
|
||
|
||
async def safe_fragment(
|
||
module_slug: str,
|
||
fn: Callable[[Request], Awaitable[str]],
|
||
request: Request,
|
||
) -> str:
|
||
try:
|
||
return await fn(request)
|
||
except Exception:
|
||
log.exception("module %s: ошибка фрагмента главной страницы", module_slug)
|
||
return (
|
||
'<aside class="module-err" role="alert">'
|
||
f"Модуль «{html.escape(module_slug)}»: блок временно недоступен."
|
||
"</aside>"
|
||
)
|