Source code for klea_utils.ui.web.nicegui.components.bootstrap

#!/usr/bin/env python3
"""
NiceGUI server bootstrap for Klea pages.

Shared, app-agnostic boilerplate: process logging, per-app NiceGUI
storage path, the ``/`` page handler (which resolves the per-browser
identity and delegates the actual composition to a *page builder*
supplied by the caller), and :func:`nicegui.ui.run`.

The page layout itself is *not* defined here -- the caller passes the
``page_builder`` callable so each app composes the page from
``klea_utils.ui.web.nicegui.components`` (ADR-0031).

File: klea_utils/ui/web/nicegui/components/bootstrap.py

Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

import logging
import os
from collections.abc import Callable
from pathlib import Path
from typing import Any

from nicegui import app, ui

from klea_utils.ui.web.nicegui.components.storage import resolve_user_id

logger = logging.getLogger(__name__)

PageBuilder = Callable[..., Any]


def _configure_logging(app_name: str) -> None:
    """Configure process-wide logging for this client process."""
    import platformdirs

    from klea_utils.plogging import resolve_log_level, setup_root_logger

    setup_root_logger(
        app_name,
        stderr_level=resolve_log_level(),
        log_dir=platformdirs.PlatformDirs(app_name).user_data_dir,
    )


def _configure_storage(app_name: str) -> None:
    """Point NiceGUI's user storage at the per-app data directory.

    Defaults to ``PlatformDirs(app_name).user_data_dir/nicegui`` when the
    deployer has not set ``NICEGUI_STORAGE_PATH``, and rebuilds
    ``app.storage`` so ``FilePersistentDict`` picks up the new path.
    """
    if "NICEGUI_STORAGE_PATH" not in os.environ:
        import platformdirs

        default_storage_dir = (
            Path(platformdirs.PlatformDirs(app_name).user_data_dir) / "nicegui"
        )
        os.environ["NICEGUI_STORAGE_PATH"] = str(default_storage_dir.resolve())
        logger.debug(
            "set default NICEGUI_STORAGE_PATH=%s", os.environ["NICEGUI_STORAGE_PATH"]
        )
    storage_dir = Path(os.environ["NICEGUI_STORAGE_PATH"]).resolve()
    try:
        storage_dir.mkdir(parents=True, exist_ok=True)
    except Exception as e:  # noqa: BLE001
        logger.warning("failed to create NICEGUI_STORAGE_PATH %s: %s", storage_dir, e)
    try:
        from nicegui.storage import Storage

        Storage.path = storage_dir
        logger.debug(
            "nicegui Storage.path set to %s (app_name=%s)", Storage.path, app_name
        )
        # Rebuild storage so FilePersistentDict picks up the new path when
        # app.storage was already instantiated at import time.
        if not app.is_started:
            app.storage = Storage()
            logger.debug("rebuilt nicegui app.storage for path %s", Storage.path)
    except Exception as e:  # noqa: BLE001
        logger.warning(
            "failed to configure NiceGUI storage path %s: %s", storage_dir, e
        )


[docs] def run_nicegui_server( title: str, server_url: str, *, page_builder: PageBuilder, subtitle: str = "", disclaimer: str = "", footer_text: str = 'Powered by <a href="https://github.com/neuroml/klea">Klea</a>', reload: bool = False, nicegui_url: str = "0.0.0.0:7860", storage_secret: str = "klea-nicegui-secret-change-me", app_name: str = "klea-web", ) -> None: """Start the NiceGUI web server with a Klea page. This is the process-level entry point for a Klea frontend. It registers a ``@ui.page("/")`` handler that resolves the per-browser ``user_id`` and delegates page composition to *page_builder* (signature ``(chat_id, user_id, server_url, title, subtitle, disclaimer, footer_text)``), then starts the NiceGUI server. Backend readiness is handled by the page itself: the layout is delivered immediately and the health probe + chat hydration run as a background task, so ``main_page`` returns within ``response_timeout`` even on a cold start. :param title: Application title (displayed in the header and browser tab). :param server_url: Base URL of the backend API server (e.g. ``http://127.0.0.1:8005``). :param page_builder: Callable that composes the page layout. :param subtitle: Optional smaller text shown next to *title*. :param disclaimer: Optional text shown below the chat input. :param footer_text: HTML content for the footer bar. :param reload: When ``True``, enable NiceGUI's file-watch hot reload. :param nicegui_url: ``host:port`` to bind the NiceGUI web server to. :param storage_secret: Secret used by NiceGUI for browser session persistence. :param app_name: Log identity for this frontend process, used as the log file name so each app keeps its own logs. """ _configure_logging(app_name) _configure_storage(app_name) host, port_str = nicegui_url.rsplit(":", 1) port = int(port_str) @ui.page("/", response_timeout=60) async def main_page(): """Build the main page without blocking on backend readiness. User identity is resolved before any ``await`` so ``app.storage.user`` is still in the request context. """ user_id = await resolve_user_id() await ui.context.client.connected() chat_id = "" logger.debug("user_id=%s chat_id=%s", user_id, chat_id) page_builder( chat_id=chat_id, user_id=user_id, server_url=server_url, title=title, subtitle=subtitle, disclaimer=disclaimer, footer_text=footer_text, ) ui.run( port=port, host=host, title=title, show=False, reload=reload, storage_secret=storage_secret, )