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

#!/usr/bin/env python3
"""
NiceGUI user-storage helpers for Klea pages.

Covers the persistent per-browser identity (``app.storage.user``) and
defends against the stale-session case where NiceGUI raises
``AssertionError`` because the backing ``storage-user-*.json`` file was
lost while the browser still sends the old session cookie.

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

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

import logging
import os
from typing import Any

from nicegui import app, core
from nicegui.storage import request_contextvar

logger = logging.getLogger(__name__)


[docs] async def ensure_user_storage(): """Return ``app.storage.user``, recreating it if stale. When the ``.nicegui/storage-user-*.json`` file is missing but the browser still sends the old session cookie, ``app.storage.user`` raises ``AssertionError``. We warn and recreate the backing ``FilePersistentDict`` for that ``session_id`` so the page can continue with a fresh ``user_id`` instead of 500. """ try: return app.storage.user except AssertionError as e: request = request_contextvar.get() session_id = request.session.get("id", "unknown") if request else "unknown" logger.warning( f"stale nicegui session {session_id = } missing storage, recreating: {e}" ) if request is not None: await core.app.storage._create_user_storage(session_id) return app.storage.user
[docs] def user_storage_or_none(): """Return ``app.storage.user`` or ``None`` if stale (no await).""" try: return app.storage.user except AssertionError as e: request = request_contextvar.get() session_id = request.session.get("id", "unknown") if request else "unknown" logger.warning(f"stale nicegui session {session_id = } at storage access: {e}") return None
[docs] def safe_set_user(key: str, value: Any) -> None: """Set ``app.storage.user[key]`` if storage is available, else warn.""" store = user_storage_or_none() if store is not None: store[key] = value else: logger.warning(f"skipping persistent set {key}={value!r} due to stale storage")
[docs] async def browser_user_id() -> str: """Return the persistent per-browser ``user_id``, creating it if missing. The multi-user fallback: identity comes from the browser's signed session cookie (``app.storage.user``), so each browser/profile is a distinct user. Must be called before any ``await`` so ``app.storage.user`` is still in the request context. :returns: The stored (or freshly generated) ``user_id`` string. """ store = await ensure_user_storage() if "user_id" not in store: import uuid store["user_id"] = str(uuid.uuid4()) logger.debug("NEW user_id=%s", store["user_id"]) else: logger.debug("EXISTING user_id=%s", store["user_id"]) return store["user_id"]
[docs] def client_is_loopback() -> bool: """Return whether the current request's client is on a loopback host. Guards the shared local identity file: a remote browser must never be handed the machine-local ``user_id`` (it would expose the local user's chats). A ``None`` client (no request context) is treated as not loopback. :returns: True when the request client host is loopback. """ # Lazy: the host helper is only needed when a request context exists. from klea_utils.net import is_loopback_host request = request_contextvar.get() if request is None or request.client is None: return False return is_loopback_host(request.client.host or "")
[docs] async def resolve_page_user_id( *, explicit: str = "", server_url: str = "", identity_app_name: str = "klea-agent", ) -> str: """Resolve the page's ``user_id`` through the shared identity seam. Order: an explicit ``--user-id`` / ``KLEA_USER_ID`` > the canonical host-local identity file (only when both the backend URL and the requesting browser are loopback, so the TUI and web share one identity for a local single-user setup) > the per-browser cookie (remote / multi-user). :param explicit: The ``--user-id`` value, or ``""``. :param server_url: Base URL of the backend API server. :param identity_app_name: Canonical identity key (the server app, e.g. ``"klea-agent"``). :returns: The resolved user id. """ # Lazy: identity pulls in platformdirs/api.server only when needed. from klea_utils.ui.identity import ( USER_ID_ENV_VAR, is_loopback_url, persisted_user_id, ) explicit = (explicit or "").strip() or os.environ.get(USER_ID_ENV_VAR, "").strip() if explicit: logger.debug("explicit user_id=%s", explicit) return explicit if is_loopback_url(server_url) and client_is_loopback(): value = persisted_user_id(identity_app_name) logger.debug("canonical local user_id=%s (app=%s)", value, identity_app_name) return value return await browser_user_id()