diff --git a/src/langbot/pkg/api/http/controller/groups/box.py b/src/langbot/pkg/api/http/controller/groups/box.py index d39ced932..d8c961e7a 100644 --- a/src/langbot/pkg/api/http/controller/groups/box.py +++ b/src/langbot/pkg/api/http/controller/groups/box.py @@ -1,6 +1,9 @@ from __future__ import annotations +from langbot.pkg.utils import constants + from .. import group +from .box_visibility import should_hide_box_runtime_status @group.group_class('box', '/api/v1/box') @@ -9,6 +12,7 @@ class BoxRouterGroup(group.RouterGroup): @self.route('/status', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: status = await self.ap.box_service.get_status() + status['hidden'] = should_hide_box_runtime_status(constants.edition, status.get('enabled')) return self.success(data=status) @self.route('/sessions', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) diff --git a/src/langbot/pkg/api/http/controller/groups/box_visibility.py b/src/langbot/pkg/api/http/controller/groups/box_visibility.py new file mode 100644 index 000000000..667203026 --- /dev/null +++ b/src/langbot/pkg/api/http/controller/groups/box_visibility.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +def should_hide_box_runtime_status(edition: str, box_enabled: bool | None) -> bool: + return edition == 'cloud' and box_enabled is False diff --git a/tests/unit_tests/api/test_box_controller.py b/tests/unit_tests/api/test_box_controller.py new file mode 100644 index 000000000..d6c968978 --- /dev/null +++ b/tests/unit_tests/api/test_box_controller.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import pytest + +from langbot.pkg.api.http.controller.groups.box_visibility import should_hide_box_runtime_status + + +@pytest.mark.parametrize( + ('edition', 'box_enabled', 'expected'), + [ + ('cloud', False, True), + ('cloud', True, False), + ('cloud', None, False), + ('community', False, False), + ], +) +def test_should_hide_box_runtime_status(edition, box_enabled, expected): + assert should_hide_box_runtime_status(edition, box_enabled) is expected diff --git a/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx b/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx index 46be50a9e..872bc90fc 100644 --- a/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx +++ b/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx @@ -65,11 +65,13 @@ export default function SystemStatusCard({ const fetchStatus = useCallback(async () => { try { - const [plugin, box, sessions] = await Promise.all([ + const [plugin, box] = await Promise.all([ httpClient.getPluginSystemStatus().catch(() => null), httpClient.getBoxStatus().catch(() => null), - httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]), ]); + const sessions = box?.hidden + ? [] + : await httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]); setPluginStatus(plugin); setBoxStatus(box); setBoxSessions(sessions); @@ -95,6 +97,7 @@ export default function SystemStatusCard({ : 'failed' : null; const boxOk = boxStatus ? boxStatus.available : null; + const hideBoxRuntime = boxStatus?.hidden === true; // Box has three observable states: connected (ok), disabled by config // (enabled = false → distinct gray dot + "disabled" hint), and configured // but failed (red dot + connector_error). The dashboard must distinguish @@ -152,11 +155,13 @@ export default function SystemStatusCard({ {t('monitoring.pluginRuntime')} -
- - - {t('monitoring.boxRuntime')} -
+ {!hideBoxRuntime && ( +
+ + + {t('monitoring.boxRuntime')} +
+ )} @@ -214,181 +219,189 @@ export default function SystemStatusCard({ -
+ {!hideBoxRuntime && ( + <> +
- {/* Box Runtime */} -
-
- - - {t('monitoring.boxRuntime')} - -
-
-
- {boxState === 'ok' ? ( - - ) : ( - - )} - - {boxState === 'ok' - ? t('monitoring.connected') - : boxState === 'disabled' - ? t('monitoring.disabled') - : t('monitoring.disconnected')} - -
- {boxState === 'disabled' && ( -

- {t('monitoring.boxDisabled')} -

- )} - {boxState === 'failed' && boxStatus?.connector_error && ( -

- {boxStatus.connector_error} -

- )} - {boxStatus && ( -
- {boxStatus.backend && ( -

- {t('monitoring.boxBackend')}:{' '} - - {boxStatus.backend.name} - -

- )} -

- {t('monitoring.boxProfile')}:{' '} - - {boxStatus.profile} - -

- {boxOk && boxStatus.active_sessions !== undefined && ( -

- {t('monitoring.boxSandboxes')}:{' '} - - {boxStatus.active_sessions} - -

- )} + {/* Box Runtime */} +
+
+ + + {t('monitoring.boxRuntime')} +
- )} - - {/* Active Sandboxes */} - {boxSessions.length > 0 && ( -
- {boxSessions.map((session) => ( -
+
+ {boxState === 'ok' ? ( + + ) : ( + + )} + -
- - - - - {session.session_id} - - - - {session.session_id} - - -
-
-
- - - - - {session.image} - - - {session.image} - -
-
- - - {session.backend_name} + {boxState === 'ok' + ? t('monitoring.connected') + : boxState === 'disabled' + ? t('monitoring.disabled') + : t('monitoring.disconnected')} + +
+ {boxState === 'disabled' && ( +

+ {t('monitoring.boxDisabled')} +

+ )} + {boxState === 'failed' && boxStatus?.connector_error && ( +

+ {boxStatus.connector_error} +

+ )} + {boxStatus && ( +
+ {boxStatus.backend && ( +

+ {t('monitoring.boxBackend')}:{' '} + + {boxStatus.backend.name} -

-
- - - {session.cpus} CPU / {session.memory_mb} MB +

+ )} +

+ {t('monitoring.boxProfile')}:{' '} + + {boxStatus.profile} + +

+ {boxOk && boxStatus.active_sessions !== undefined && ( +

+ {t('monitoring.boxSandboxes')}:{' '} + + {boxStatus.active_sessions} -

-
- - - {session.network} - -
- {session.host_path && ( -
- +

+ )} +
+ )} + + {/* Active Sandboxes */} + {boxSessions.length > 0 && ( +
+ {boxSessions.map((session) => ( +
+
+ - - {session.host_path} : {session.mount_path}{' '} - - ({session.host_path_mode}) - + + {session.session_id} - {session.host_path} : {session.mount_path} ( - {session.host_path_mode}) + {session.session_id}
- )} -
- - - {t('monitoring.boxSessionCreated')}:{' '} - - {new Date( - session.created_at, - ).toLocaleString()} - - +
+
+ + + + + {session.image} + + + + {session.image} + + +
+
+ + + {session.backend_name} + +
+
+ + + {session.cpus} CPU / {session.memory_mb} MB + +
+
+ + + {session.network} + +
+ {session.host_path && ( +
+ + + + + {session.host_path} :{' '} + {session.mount_path}{' '} + + ({session.host_path_mode}) + + + + + {session.host_path} :{' '} + {session.mount_path} ( + {session.host_path_mode}) + + +
+ )} +
+ + + {t('monitoring.boxSessionCreated')}:{' '} + + {new Date( + session.created_at, + ).toLocaleString()} + + +
+
+ + + {t('monitoring.boxSessionLastUsed')}:{' '} + + {new Date( + session.last_used_at, + ).toLocaleString()} + + +
+
-
- - - {t('monitoring.boxSessionLastUsed')}:{' '} - - {new Date( - session.last_used_at, - ).toLocaleString()} - - -
-
+ ))}
- ))} + )}
- )} -
-
+
+ + )}
diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 582364656..61cb33acd 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -373,6 +373,8 @@ export interface ApiRespPluginSystemStatus { export interface ApiRespBoxStatus { available: boolean; + /** UI hint: hide the Box runtime status surface for this deployment. */ + hidden?: boolean; /** Whether ``box.enabled`` is true in config. When false, the sandbox * is deliberately disabled — distinct from "configured but failed". */ enabled?: boolean;