v1.4.0: модули с веб-UI, правое меню, расширенные тесты

Реестр MODULE_MOUNTS: API, ui_router, фрагменты главной, EventBus.
Главная и страницы модулей с правой навигацией из реестра; wrap_module_html_page.
Ingress: публикация alert.received после сохранения в БД.
Документация MODULES.md; pytest покрывает API, UI и навигацию.

Made-with: Cursor
This commit is contained in:
Alexandr
2026-04-03 08:45:19 +03:00
parent 85eb61b576
commit 349cea85a3
17 changed files with 571 additions and 24 deletions

View File

@ -0,0 +1,71 @@
"""Единая точка регистрации модулей: API, веб-UI и подписки на события.
Новый модуль: файл в `onguard24/modules/`, запись в `MODULE_MOUNTS` — см. docs/MODULES.md.
"""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from fastapi import APIRouter
from starlette.requests import Request
from onguard24.domain.events import EventBus
from onguard24.modules import contacts, schedules, statusboard
# async (Request) -> str — фрагмент HTML для главной страницы (опционально)
HomeFragment = Callable[[Request], Awaitable[str]]
@dataclass(frozen=True)
class ModuleMount:
"""Один модуль: API под url_prefix, UI под /ui/modules/{slug}."""
router: APIRouter
url_prefix: str
register_events: Callable[[EventBus], None]
slug: str
title: str
ui_router: APIRouter | None = None
render_home_fragment: HomeFragment | None = None
def _mounts() -> list[ModuleMount]:
return [
ModuleMount(
router=schedules.router,
url_prefix="/api/v1/modules/schedules",
register_events=schedules.register_events,
slug="schedules",
title="Календарь дежурств",
ui_router=schedules.ui_router,
render_home_fragment=schedules.render_home_fragment,
),
ModuleMount(
router=contacts.router,
url_prefix="/api/v1/modules/contacts",
register_events=contacts.register_events,
slug="contacts",
title="Контакты",
ui_router=contacts.ui_router,
render_home_fragment=contacts.render_home_fragment,
),
ModuleMount(
router=statusboard.router,
url_prefix="/api/v1/modules/statusboard",
register_events=statusboard.register_events,
slug="statusboard",
title="Светофор",
ui_router=statusboard.ui_router,
render_home_fragment=statusboard.render_home_fragment,
),
]
MODULE_MOUNTS: list[ModuleMount] = _mounts()
def register_module_events(bus: EventBus) -> None:
for m in MODULE_MOUNTS:
m.register_events(bus)