From f96116a050d93e73dcca91a3b7a6c7fb6445daa0 Mon Sep 17 00:00:00 2001 From: dadachann <185672915+dadachann@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:42:05 +0800 Subject: [PATCH] fix(cloud): surface runtime and workspace plan status --- .../pkg/api/http/controller/groups/box.py | 12 +++++++ .../api/http/controller/groups/workspaces.py | 33 +++++++++++-------- tests/integration/api/test_box_security.py | 18 ++++++++++ tests/integration/api/test_workspaces.py | 6 ++++ .../components/home-sidebar/HomeSidebar.tsx | 2 +- .../WorkspaceSettingsPanel.tsx | 30 ++++++++++------- .../workspace-settings/WorkspaceSwitcher.tsx | 13 +++----- .../overview-cards/SystemStatusCards.tsx | 2 +- web/src/app/infra/http/BackendClient.ts | 4 +++ .../unit/workspace-switcher-layout.test.mjs | 25 ++++++++++++-- 10 files changed, 108 insertions(+), 37 deletions(-) diff --git a/src/langbot/pkg/api/http/controller/groups/box.py b/src/langbot/pkg/api/http/controller/groups/box.py index 4a7c87ed1..d63e9d7b5 100644 --- a/src/langbot/pkg/api/http/controller/groups/box.py +++ b/src/langbot/pkg/api/http/controller/groups/box.py @@ -27,6 +27,18 @@ class BoxRouterGroup(group.RouterGroup): status['hidden'] = should_hide_box_runtime_status(constants.edition, status.get('enabled')) return self.success(data=status) + @self.route( + '/runtime-status', + methods=['GET'], + auth_type=group.AuthType.USER_TOKEN, + permission=Permission.RESOURCE_VIEW, + ) + async def _(request_context: RequestContext) -> str: + del request_context + status = await self.ap.box_service.get_backend_status() + status['hidden'] = should_hide_box_runtime_status(constants.edition, status.get('enabled')) + return self.success(data=status) + @self.route( '/sessions', methods=['GET'], diff --git a/src/langbot/pkg/api/http/controller/groups/workspaces.py b/src/langbot/pkg/api/http/controller/groups/workspaces.py index 4917d5db9..72964c881 100644 --- a/src/langbot/pkg/api/http/controller/groups/workspaces.py +++ b/src/langbot/pkg/api/http/controller/groups/workspaces.py @@ -70,19 +70,26 @@ class WorkspacesRouterGroup(group.RouterGroup): """ accesses = await self.ap.workspace_collaboration_service.list_account_workspaces(account.uuid) - return self.success( - data={ - 'workspaces': [ - { - 'workspace': _workspace_payload(access.workspace), - 'membership': _membership_payload(access.membership, email=account.user), - 'permissions': sorted(permissions_for_role(access.membership.role)), - 'placement_generation': access.execution.placement_generation, - } - for access in accesses - ] - } - ) + resolver = getattr(self.ap, 'entitlement_resolver', None) + workspaces: list[dict[str, typing.Any]] = [] + for access in accesses: + plan_name: str | None = None + if access.workspace.source == WorkspaceSource.CLOUD_PROJECTION.value and resolver is not None: + entitlement = await resolver.resolve( + access.workspace.uuid, + minimum_revision=access.membership.projection_revision, + ) + plan_name = entitlement.plan_name + workspaces.append( + { + 'workspace': _workspace_payload(access.workspace), + 'membership': _membership_payload(access.membership, email=account.user), + 'permissions': sorted(permissions_for_role(access.membership.role)), + 'placement_generation': access.execution.placement_generation, + 'plan_name': plan_name, + } + ) + return self.success(data={'workspaces': workspaces}) @self.route('', methods=['GET'], auth_type=group.AuthType.ACCOUNT_TOKEN) async def _(account) -> typing.Any: diff --git a/tests/integration/api/test_box_security.py b/tests/integration/api/test_box_security.py index f0a901366..0a40f230a 100644 --- a/tests/integration/api/test_box_security.py +++ b/tests/integration/api/test_box_security.py @@ -42,6 +42,9 @@ async def box_security_api(): side_effect=lambda account_uuid, _workspace_uuid: _access(account_uuid) ) application.box_service.get_status = AsyncMock(return_value={'enabled': True}) + application.box_service.get_backend_status = AsyncMock( + return_value={'available': True, 'enabled': True, 'backend': {'name': 'nsjail'}} + ) application.box_service.get_sessions = AsyncMock(return_value=[{'session_id': 'private-session'}]) application.box_service.get_recent_errors = Mock(return_value=[{'error': 'private error'}]) application.box_service.managed_admission_required = False @@ -99,3 +102,18 @@ async def test_box_status_returns_explicit_403_when_workspace_has_no_managed_san assert response.status_code == 403 payload = await response.get_json() assert payload['code'] == 'managed_sandbox_unavailable' + + +@pytest.mark.asyncio +async def test_runtime_status_reports_connector_health_without_consuming_workspace_entitlement(box_security_api): + application, client = box_security_api + application.box_service.get_status.side_effect = EntitlementUnavailableError( + 'Workspace entitlement does not grant managed_sandbox' + ) + + response = await client.get('/api/v1/box/runtime-status', headers=_headers('viewer-token')) + + assert response.status_code == 200 + assert (await response.get_json())['data']['available'] is True + application.box_service.get_backend_status.assert_awaited_once() + application.box_service.get_status.assert_not_awaited() diff --git a/tests/integration/api/test_workspaces.py b/tests/integration/api/test_workspaces.py index 129bdbedc..9ec261870 100644 --- a/tests/integration/api/test_workspaces.py +++ b/tests/integration/api/test_workspaces.py @@ -484,6 +484,12 @@ async def test_cloud_projection_is_selected_explicitly_and_directory_writes_use_ ) application.entitlement_resolver = PlanResolver() + bootstrap_with_plans = await client.get('/api/v1/workspaces/bootstrap', headers=_auth(owner_token)) + bootstrap_by_uuid = { + item['workspace']['uuid']: item for item in (await bootstrap_with_plans.get_json())['data']['workspaces'] + } + assert bootstrap_by_uuid[cloud_workspace_uuid]['plan_name'] == 'free' + assert bootstrap_by_uuid[singleton_uuid]['plan_name'] is None current_response = await client.get( '/api/v1/workspaces/current', headers=_auth(owner_token, cloud_workspace_uuid), diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 75da46850..99df6f7f6 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -1916,7 +1916,7 @@ export default function HomeSidebar({