"""Безопасная сборка 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; } .irm-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } .irm-table th, .irm-table td { border: 1px solid #e4e4e7; padding: 0.45rem 0.65rem; text-align: left; } .irm-table thead th { background: #f4f4f5; } .og-btn { padding: 0.45rem 0.9rem; font-size: 0.875rem; border-radius: 6px; border: 1px solid #d4d4d8; background: #fff; cursor: pointer; margin-right: 0.5rem; } .og-btn:hover { background: #f4f4f5; } .og-btn-primary { background: #2563eb; color: #fff; border-color: #1d4ed8; } .og-btn-primary:hover { background: #1d4ed8; } .og-sync-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 1rem 0; } .og-sync-status { font-size: 0.85rem; color: #52525b; min-height: 1.25rem; } .gc-tree { margin-top: 1.25rem; } .gc-folder { margin: 0.4rem 0; border: 1px solid #e4e4e7; border-radius: 8px; padding: 0.35rem 0.6rem; background: #fff; } .gc-folder summary { cursor: pointer; list-style: none; } .gc-folder summary::-webkit-details-marker { display: none; } .gc-muted { color: #71717a; font-size: 0.82rem; font-weight: normal; } .gc-subtable { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin: 0.5rem 0 0.25rem; } .gc-subtable th, .gc-subtable td { border: 1px solid #e4e4e7; padding: 0.3rem 0.45rem; } .gc-subtable th { background: #fafafa; } .gc-orphan { margin-top: 1rem; padding: 0.75rem; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px; font-size: 0.88rem; } .rail-item--util { border-top: 1px solid #e4e4e7; margin-top: 0.4rem; padding-top: 0.4rem; } """ def nav_rail_html(current_slug: str | None = None) -> str: """Правая колонка: пункты из реестра модулей с `ui_router` + «Главная». Новый модуль в `MODULE_MOUNTS` с UI — пункт появляется автоматически. """ from onguard24.modules.registry import MODULE_MOUNTS home_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'
  • {html.escape(m.title)}
  • ' ) logs_active = current_slug == "__logs__" items.append( '
  • ' '📋 Логи
  • " ) lis = "".join(items) return ( '" ) 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""" {html.escape(document_title)}
    {main_inner_html}
    {rail}
    """ 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 ( '" )