mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-12 05:40:58 +00:00
fix(monitoring): hide disabled box status on cloud
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user