mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-23 14:04:19 +00:00
fix(monitoring): hide disabled box status on cloud
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 () => {
|
||||
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({
|
||||
<Plug className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-sm">{t('monitoring.pluginRuntime')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusDot state={boxState} />
|
||||
<Box className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-sm">{t('monitoring.boxRuntime')}</span>
|
||||
</div>
|
||||
{!hideBoxRuntime && (
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusDot state={boxState} />
|
||||
<Box className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-sm">{t('monitoring.boxRuntime')}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -214,181 +219,189 @@ export default function SystemStatusCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t" />
|
||||
{!hideBoxRuntime && (
|
||||
<>
|
||||
<div className="border-t" />
|
||||
|
||||
{/* Box Runtime */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Box className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-semibold">
|
||||
{t('monitoring.boxRuntime')}
|
||||
</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>
|
||||
)}
|
||||
{/* Box Runtime */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Box className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-semibold">
|
||||
{t('monitoring.boxRuntime')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Sandboxes */}
|
||||
{boxSessions.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{boxSessions.map((session) => (
|
||||
<div
|
||||
key={session.session_id}
|
||||
className="rounded-lg border p-3 space-y-2"
|
||||
<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'
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<Container className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="font-mono font-semibold text-foreground truncate text-sm">
|
||||
{session.session_id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{session.session_id}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</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 min-w-0">
|
||||
<Image className="w-3 h-3 flex-shrink-0" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-foreground font-mono truncate">
|
||||
{session.image}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{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}
|
||||
{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>
|
||||
</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
|
||||
</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>
|
||||
</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" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Sandboxes */}
|
||||
{boxSessions.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{boxSessions.map((session) => (
|
||||
<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>
|
||||
<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 className="font-mono font-semibold text-foreground truncate text-sm">
|
||||
{session.session_id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{session.host_path} : {session.mount_path} (
|
||||
{session.host_path_mode})
|
||||
{session.session_id}
|
||||
</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 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 min-w-0">
|
||||
<Image className="w-3 h-3 flex-shrink-0" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-foreground font-mono truncate">
|
||||
{session.image}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{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 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>
|
||||
</TooltipProvider>
|
||||
</DialogContent>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user