fix(monitoring): hide disabled box status on cloud

This commit is contained in:
RockChinQ
2026-06-23 06:40:05 -04:00
parent a43978ff24
commit 59b2a7cd51
5 changed files with 208 additions and 166 deletions
@@ -1,6 +1,9 @@
from __future__ import annotations from __future__ import annotations
from langbot.pkg.utils import constants
from .. import group from .. import group
from .box_visibility import should_hide_box_runtime_status
@group.group_class('box', '/api/v1/box') @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) @self.route('/status', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
status = await self.ap.box_service.get_status() 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) return self.success(data=status)
@self.route('/sessions', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) @self.route('/sessions', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
@@ -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
@@ -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
@@ -65,11 +65,13 @@ export default function SystemStatusCard({
const fetchStatus = useCallback(async () => { const fetchStatus = useCallback(async () => {
try { try {
const [plugin, box, sessions] = await Promise.all([ const [plugin, box] = await Promise.all([
httpClient.getPluginSystemStatus().catch(() => null), httpClient.getPluginSystemStatus().catch(() => null),
httpClient.getBoxStatus().catch(() => null), httpClient.getBoxStatus().catch(() => null),
httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]),
]); ]);
const sessions = box?.hidden
? []
: await httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]);
setPluginStatus(plugin); setPluginStatus(plugin);
setBoxStatus(box); setBoxStatus(box);
setBoxSessions(sessions); setBoxSessions(sessions);
@@ -95,6 +97,7 @@ export default function SystemStatusCard({
: 'failed' : 'failed'
: null; : null;
const boxOk = boxStatus ? boxStatus.available : null; const boxOk = boxStatus ? boxStatus.available : null;
const hideBoxRuntime = boxStatus?.hidden === true;
// Box has three observable states: connected (ok), disabled by config // Box has three observable states: connected (ok), disabled by config
// (enabled = false → distinct gray dot + "disabled" hint), and configured // (enabled = false → distinct gray dot + "disabled" hint), and configured
// but failed (red dot + connector_error). The dashboard must distinguish // but failed (red dot + connector_error). The dashboard must distinguish
@@ -152,11 +155,13 @@ export default function SystemStatusCard({
<Plug className="w-3.5 h-3.5 text-muted-foreground" /> <Plug className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-sm">{t('monitoring.pluginRuntime')}</span> <span className="text-sm">{t('monitoring.pluginRuntime')}</span>
</div> </div>
<div className="flex items-center gap-2"> {!hideBoxRuntime && (
<StatusDot state={boxState} /> <div className="flex items-center gap-2">
<Box className="w-3.5 h-3.5 text-muted-foreground" /> <StatusDot state={boxState} />
<span className="text-sm">{t('monitoring.boxRuntime')}</span> <Box className="w-3.5 h-3.5 text-muted-foreground" />
</div> <span className="text-sm">{t('monitoring.boxRuntime')}</span>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
@@ -214,181 +219,189 @@ export default function SystemStatusCard({
</div> </div>
</div> </div>
<div className="border-t" /> {!hideBoxRuntime && (
<>
<div className="border-t" />
{/* Box Runtime */} {/* Box Runtime */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Box className="w-4 h-4 text-muted-foreground" /> <Box className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-semibold"> <span className="text-sm font-semibold">
{t('monitoring.boxRuntime')} {t('monitoring.boxRuntime')}
</span> </span>
</div>
<div className="ml-6 text-sm space-y-1">
<div className="flex items-center gap-1.5">
{boxState === 'ok' ? (
<CircleCheck className="w-4 h-4 text-green-600" />
) : (
<CircleX
className={
boxState === 'disabled'
? 'w-4 h-4 text-muted-foreground'
: 'w-4 h-4 text-red-500'
}
/>
)}
<span
className={
boxState === 'ok'
? 'text-green-600 font-medium'
: boxState === 'disabled'
? 'text-muted-foreground font-medium'
: 'text-red-500 font-medium'
}
>
{boxState === 'ok'
? t('monitoring.connected')
: boxState === 'disabled'
? t('monitoring.disabled')
: t('monitoring.disconnected')}
</span>
</div>
{boxState === 'disabled' && (
<p className="text-muted-foreground text-xs">
{t('monitoring.boxDisabled')}
</p>
)}
{boxState === 'failed' && boxStatus?.connector_error && (
<p className="text-red-400 text-xs break-all">
{boxStatus.connector_error}
</p>
)}
{boxStatus && (
<div className="text-muted-foreground text-xs space-y-0.5">
{boxStatus.backend && (
<p>
{t('monitoring.boxBackend')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.backend.name}
</span>
</p>
)}
<p>
{t('monitoring.boxProfile')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.profile}
</span>
</p>
{boxOk && boxStatus.active_sessions !== undefined && (
<p>
{t('monitoring.boxSandboxes')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.active_sessions}
</span>
</p>
)}
</div> </div>
)} <div className="ml-6 text-sm space-y-1">
<div className="flex items-center gap-1.5">
{/* Active Sandboxes */} {boxState === 'ok' ? (
{boxSessions.length > 0 && ( <CircleCheck className="w-4 h-4 text-green-600" />
<div className="mt-3 space-y-2"> ) : (
{boxSessions.map((session) => ( <CircleX
<div className={
key={session.session_id} boxState === 'disabled'
className="rounded-lg border p-3 space-y-2" ? 'w-4 h-4 text-muted-foreground'
: 'w-4 h-4 text-red-500'
}
/>
)}
<span
className={
boxState === 'ok'
? 'text-green-600 font-medium'
: boxState === 'disabled'
? 'text-muted-foreground font-medium'
: 'text-red-500 font-medium'
}
> >
<div className="flex items-center gap-1.5 min-w-0"> {boxState === 'ok'
<Container className="w-4 h-4 text-muted-foreground flex-shrink-0" /> ? t('monitoring.connected')
<Tooltip> : boxState === 'disabled'
<TooltipTrigger asChild> ? t('monitoring.disabled')
<span className="font-mono font-semibold text-foreground truncate text-sm"> : t('monitoring.disconnected')}
{session.session_id} </span>
</span> </div>
</TooltipTrigger> {boxState === 'disabled' && (
<TooltipContent> <p className="text-muted-foreground text-xs">
{session.session_id} {t('monitoring.boxDisabled')}
</TooltipContent> </p>
</Tooltip> )}
</div> {boxState === 'failed' && boxStatus?.connector_error && (
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-xs"> <p className="text-red-400 text-xs break-all">
<div className="flex items-center gap-1.5 text-muted-foreground min-w-0"> {boxStatus.connector_error}
<Image className="w-3 h-3 flex-shrink-0" /> </p>
<Tooltip> )}
<TooltipTrigger asChild> {boxStatus && (
<span className="text-foreground font-mono truncate"> <div className="text-muted-foreground text-xs space-y-0.5">
{session.image} {boxStatus.backend && (
</span> <p>
</TooltipTrigger> {t('monitoring.boxBackend')}:{' '}
<TooltipContent>{session.image}</TooltipContent> <span className="text-foreground font-mono">
</Tooltip> {boxStatus.backend.name}
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<HardDrive className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground">
{session.backend_name}
</span> </span>
</div> </p>
<div className="flex items-center gap-1.5 text-muted-foreground"> )}
<Cpu className="w-3 h-3 flex-shrink-0" /> <p>
<span className="text-foreground"> {t('monitoring.boxProfile')}:{' '}
{session.cpus} CPU / {session.memory_mb} MB <span className="text-foreground font-mono">
{boxStatus.profile}
</span>
</p>
{boxOk && boxStatus.active_sessions !== undefined && (
<p>
{t('monitoring.boxSandboxes')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.active_sessions}
</span> </span>
</div> </p>
<div className="flex items-center gap-1.5 text-muted-foreground"> )}
<Network className="w-3 h-3 flex-shrink-0" /> </div>
<span className="text-foreground"> )}
{session.network}
</span> {/* Active Sandboxes */}
</div> {boxSessions.length > 0 && (
{session.host_path && ( <div className="mt-3 space-y-2">
<div className="flex items-center gap-1.5 text-muted-foreground col-span-2 min-w-0"> {boxSessions.map((session) => (
<FolderOpen className="w-3 h-3 flex-shrink-0" /> <div
key={session.session_id}
className="rounded-lg border p-3 space-y-2"
>
<div className="flex items-center gap-1.5 min-w-0">
<Container className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="text-foreground font-mono truncate"> <span className="font-mono font-semibold text-foreground truncate text-sm">
{session.host_path} : {session.mount_path}{' '} {session.session_id}
<span className="text-muted-foreground">
({session.host_path_mode})
</span>
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{session.host_path} : {session.mount_path} ( {session.session_id}
{session.host_path_mode})
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
)} <div className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-xs">
<div className="flex items-center gap-1.5 text-muted-foreground"> <div className="flex items-center gap-1.5 text-muted-foreground min-w-0">
<Clock className="w-3 h-3 flex-shrink-0" /> <Image className="w-3 h-3 flex-shrink-0" />
<span> <Tooltip>
{t('monitoring.boxSessionCreated')}:{' '} <TooltipTrigger asChild>
<span className="text-foreground"> <span className="text-foreground font-mono truncate">
{new Date( {session.image}
session.created_at, </span>
).toLocaleString()} </TooltipTrigger>
</span> <TooltipContent>
</span> {session.image}
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<HardDrive className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground">
{session.backend_name}
</span>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Cpu className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground">
{session.cpus} CPU / {session.memory_mb} MB
</span>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Network className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground">
{session.network}
</span>
</div>
{session.host_path && (
<div className="flex items-center gap-1.5 text-muted-foreground col-span-2 min-w-0">
<FolderOpen className="w-3 h-3 flex-shrink-0" />
<Tooltip>
<TooltipTrigger asChild>
<span className="text-foreground font-mono truncate">
{session.host_path} :{' '}
{session.mount_path}{' '}
<span className="text-muted-foreground">
({session.host_path_mode})
</span>
</span>
</TooltipTrigger>
<TooltipContent>
{session.host_path} :{' '}
{session.mount_path} (
{session.host_path_mode})
</TooltipContent>
</Tooltip>
</div>
)}
<div className="flex items-center gap-1.5 text-muted-foreground">
<Clock className="w-3 h-3 flex-shrink-0" />
<span>
{t('monitoring.boxSessionCreated')}:{' '}
<span className="text-foreground">
{new Date(
session.created_at,
).toLocaleString()}
</span>
</span>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Clock className="w-3 h-3 flex-shrink-0" />
<span>
{t('monitoring.boxSessionLastUsed')}:{' '}
<span className="text-foreground">
{new Date(
session.last_used_at,
).toLocaleString()}
</span>
</span>
</div>
</div>
</div> </div>
<div className="flex items-center gap-1.5 text-muted-foreground"> ))}
<Clock className="w-3 h-3 flex-shrink-0" />
<span>
{t('monitoring.boxSessionLastUsed')}:{' '}
<span className="text-foreground">
{new Date(
session.last_used_at,
).toLocaleString()}
</span>
</span>
</div>
</div>
</div> </div>
))} )}
</div> </div>
)} </div>
</div> </>
</div> )}
</div> </div>
</TooltipProvider> </TooltipProvider>
</DialogContent> </DialogContent>
+2
View File
@@ -373,6 +373,8 @@ export interface ApiRespPluginSystemStatus {
export interface ApiRespBoxStatus { export interface ApiRespBoxStatus {
available: boolean; 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 /** Whether ``box.enabled`` is true in config. When false, the sandbox
* is deliberately disabled — distinct from "configured but failed". */ * is deliberately disabled — distinct from "configured but failed". */
enabled?: boolean; enabled?: boolean;