"""HTTP surface for the mobile app-grid catalog. The catalog data itself lives in :mod:`app.mobile.apps_catalog`; this module is the thin endpoint adapter that filters by the caller's roles and packages the result into the response envelope. """ from __future__ import annotations from fastapi import APIRouter, Depends, Query, Request from app.auth.models import AuthUser from app.core.dependencies import get_current_user from app.core.response import success_response from app.mobile import apps_catalog from app.mobile.perspective import resolve_perspective_role_names from app.mobile.schemas import ( MobileAppCategory, MobileAppEntry, MobileAppsCatalogResponse, ) router = APIRouter(prefix="/apps", tags=["mobile"]) @router.get("/catalog", summary="Mobile app grid: categories + role-filtered apps") async def list_apps_catalog( request: Request, current_user: AuthUser = Depends(get_current_user), as_role: str | None = Query( None, description=( "Optional perspective to filter by — collapses the catalog to a " "single role's apps for the mobile RoleSwitcher. Ignored if the " "caller doesn't hold the requested role." ), ), ): """Return the apps the caller can navigate to from the mobile grid. The response is intentionally idempotent and side-effect-free — the client can cache it for the session and refresh on RoleSwitcher change. """ user_role_names = list(current_user.role_names or []) role_names = resolve_perspective_role_names(user_role_names, as_role) visible = apps_catalog.visible_apps_for(role_names) pinned = apps_catalog.default_pinned_keys_for(role_names) payload = MobileAppsCatalogResponse( categories=[ MobileAppCategory( key=c["key"], title_key=c["title_key"], order=c["order"], ) for c in apps_catalog.CATEGORIES ], apps=[ MobileAppEntry( key=a["key"], category=a["category"], title_key=a["title_key"], subtitle_key=a.get("subtitle_key"), icon=a["icon"], path=a["path"], ) for a in visible ], pinned_default=pinned, ) return success_response(data=payload.model_dump(), request=request)