jmri_web

jmri-web — Stage 5 of the JMRI Python rewrite.

A server-rendered web UI built on htmx + Jinja2 + FastAPI. Mounts on top of jmri-server so a single process serves both the JSON wire API and the human-facing pages, sharing one in-memory InternalConnection (or a driver-backed one, once Stage 5.5 wires connection selection).

 1"""jmri-web — Stage 5 of the JMRI Python rewrite.
 2
 3A server-rendered web UI built on htmx + Jinja2 + FastAPI. Mounts on top
 4of ``jmri-server`` so a single process serves both the JSON wire API and
 5the human-facing pages, sharing one in-memory ``InternalConnection`` (or
 6a driver-backed one, once Stage 5.5 wires connection selection).
 7"""
 8from jmri_web.app import build_web_app, create_app
 9
10__version__ = "0.1.0"
11
12__all__ = ["__version__", "build_web_app", "create_app"]
__version__ = '0.1.0'
def build_web_app( *, metadata: jmri_server.context.ServerMetadata | None = None) -> jmri_web.app.WebHandle:
33def build_web_app(*, metadata: ServerMetadata | None = None) -> WebHandle:
34    server = build_server_app(metadata=metadata)
35    app = server.app
36
37    # Static assets + Jinja2 templates.
38    if STATIC_DIR.is_dir():
39        app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
40        # Generated pdoc reference lives at ``static/docs/``. Mounting
41        # it directly at ``/docs/`` gives a cleaner URL for the "read
42        # the docs" links in topic-help panels + the OVERVIEW page.
43        # When docs haven't been built yet the directory is missing;
44        # skip the mount silently so ``build_docs.py`` remains an
45        # optional developer step, not a hard startup requirement.
46        docs_dir = STATIC_DIR / "docs"
47        if docs_dir.is_dir():
48            app.mount(
49                "/docs",
50                StaticFiles(directory=str(docs_dir), html=True),
51                name="docs",
52            )
53    templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
54
55    install_template_routes(app, server, templates)
56
57    # Auto-load the decoder catalog when ``JMRI_DECODER_XML_ROOT`` is
58    # set. Chained onto the server's existing lifespan so it runs
59    # AFTER server startup (config store, hardware connection, etc.
60    # are all up) but before any user request lands. The load itself
61    # runs in a background thread so startup stays fast — pages that
62    # check ``len(catalog) > 0`` see an empty catalog until the scan
63    # completes (same as the "user hasn't clicked Load yet" state).
64    #
65    # FastAPI removed ``add_event_handler`` in newer releases; the
66    # lifespan-chaining pattern is the supported replacement.
67    from jmri_web.catalog_autoload import schedule_autoload  # noqa: PLC0415
68
69    _server_lifespan = app.router.lifespan_context
70
71    @asynccontextmanager
72    async def _web_lifespan(_app):  # type: ignore[no-untyped-def]
73        async with _server_lifespan(_app):
74            schedule_autoload(server)
75            yield
76
77    app.router.lifespan_context = _web_lifespan
78
79    return WebHandle(app=app, server=server, templates=templates)
def create_app() -> fastapi.applications.FastAPI:
 82def create_app() -> FastAPI:
 83    """Default ASGI entry point. ``uvicorn jmri_web:create_app --factory``.
 84
 85    Production wiring: builds the full web + server stack and attaches
 86    the multi-profile registry + ``ConfigStore`` (reading
 87    ``<settings>/profiles.xml``, honouring ``JMRI_SETTINGS_DIR``).
 88    Tests that go through :func:`build_web_app` directly skip this for
 89    hermeticity.
 90    """
 91    from jmri_core.persistence import default_settings_dir  # noqa: PLC0415
 92    from jmri_server.app import (  # noqa: PLC0415
 93        attach_auth,
 94        attach_profile_persistence,
 95        attach_withrottle_server,
 96    )
 97    handle = build_web_app()
 98    attach_profile_persistence(handle.server)
 99    attach_withrottle_server(handle.server)
100    # Auth is opt-in via ``JMRI_AUTH_ENABLED=1`` — the attach is a
101    # no-op when the env var isn't set, so this line is safe to
102    # keep unconditionally.
103    attach_auth(handle.server, default_settings_dir())
104    return handle.app

Default ASGI entry point. uvicorn jmri_web:create_app --factory.

Production wiring: builds the full web + server stack and attaches the multi-profile registry + ConfigStore (reading <settings>/profiles.xml, honouring JMRI_SETTINGS_DIR). Tests that go through build_web_app() directly skip this for hermeticity.