feat(web): show active sandbox details in Box status popover

Display sandbox count and a detailed list of active sessions including
session ID, image, backend, resources (CPU/memory), network mode, and
last used time. Fetched from GET /api/v1/box/sessions in parallel.
Includes i18n for all 8 supported languages.
This commit is contained in:
Junyan Qin
2026-04-18 22:16:36 +08:00
committed by WangCham
parent 7e50063731
commit cee5e9e0e2
11 changed files with 126 additions and 1 deletions

View File

@@ -20,6 +20,8 @@ import {
Box,
CircleCheck,
CircleX,
Container,
Clock,
} from 'lucide-react';
import {
DropdownMenu,
@@ -49,6 +51,7 @@ import { systemInfo } from '@/app/infra/http/HttpClient';
import {
ApiRespPluginSystemStatus,
ApiRespBoxStatus,
BoxSessionInfo,
} from '@/app/infra/entities/api';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import {
@@ -140,6 +143,7 @@ function PluginListView() {
const [copiedDebugUrl, setCopiedDebugUrl] = useState(false);
const [copiedDebugKey, setCopiedDebugKey] = useState(false);
const [boxStatus, setBoxStatus] = useState<ApiRespBoxStatus | null>(null);
const [boxSessions, setBoxSessions] = useState<BoxSessionInfo[]>([]);
useEffect(() => {
const fetchPluginSystemStatus = async () => {
@@ -461,12 +465,14 @@ function PluginListView() {
const handleShowDebugInfo = async () => {
try {
const [info, box] = await Promise.all([
const [info, box, sessions] = await Promise.all([
httpClient.getPluginDebugInfo(),
httpClient.getBoxStatus().catch(() => null),
httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]),
]);
setDebugInfo(info);
setBoxStatus(box);
setBoxSessions(sessions);
setDebugPopoverOpen(true);
} catch (error) {
console.error('Failed to fetch debug info:', error);
@@ -702,6 +708,14 @@ function PluginListView() {
</span>
<span className="font-mono">{boxStatus.profile}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[80px]">
{t('plugins.boxSandboxes')}:
</span>
<span className="font-mono">
{boxStatus.active_sessions ?? 0}
</span>
</div>
{(boxStatus.recent_error_count ?? 0) > 0 && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[80px]">
@@ -712,6 +726,58 @@ function PluginListView() {
</span>
</div>
)}
{boxSessions.length > 0 && (
<div className="pt-1.5 mt-1.5 border-t border-dashed space-y-2">
{boxSessions.map((session) => (
<div
key={session.session_id}
className="rounded border p-2 space-y-1"
>
<div className="flex items-center gap-1.5">
<Container className="w-3 h-3 text-muted-foreground" />
<span className="font-mono font-medium truncate">
{session.session_id}
</span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-muted-foreground">
<span>
{t('plugins.boxSessionImage')}:{' '}
<span className="text-foreground font-mono">
{session.image.split(':').pop() ||
session.image}
</span>
</span>
<span>
{t('plugins.boxSessionBackend')}:{' '}
<span className="text-foreground font-mono">
{session.backend_name}
</span>
</span>
<span>
{t('plugins.boxSessionResources')}:{' '}
<span className="text-foreground font-mono">
{session.cpus} CPU / {session.memory_mb}MB
</span>
</span>
<span>
{t('plugins.boxSessionNetwork')}:{' '}
<span className="text-foreground font-mono">
{session.network}
</span>
</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground">
<Clock className="w-3 h-3" />
<span>
{new Date(
session.last_used_at,
).toLocaleString()}
</span>
</div>
</div>
))}
</div>
)}
</div>
) : (
<p className="text-xs text-muted-foreground">

View File

@@ -364,6 +364,20 @@ export interface ApiRespBoxStatus {
session_ttl_sec?: number;
}
export interface BoxSessionInfo {
session_id: string;
backend_name: string;
image: string;
network: string;
host_path: string | null;
host_path_mode: string;
mount_path: string;
cpus: number;
memory_mb: number;
created_at: string;
last_used_at: string;
}
export interface ApiRespAsyncTasks {
tasks: AsyncTask[];
}

View File

@@ -36,6 +36,7 @@ import {
RerankModel,
ApiRespPluginSystemStatus,
ApiRespBoxStatus,
BoxSessionInfo,
ApiRespMCPServers,
ApiRespMCPServer,
MCPServer,
@@ -894,6 +895,10 @@ export class BackendClient extends BaseHttpClient {
return this.get('/api/v1/box/status');
}
public getBoxSessions(): Promise<BoxSessionInfo[]> {
return this.get('/api/v1/box/sessions');
}
// ============ User API ============
public checkIfInited(): Promise<{ initialized: boolean }> {
return this.get('/api/v1/user/init');

View File

@@ -473,7 +473,12 @@ const enUS = {
boxUnavailable: 'Unavailable',
boxBackend: 'Backend',
boxProfile: 'Profile',
boxSandboxes: 'Sandboxes',
boxErrors: 'Errors',
boxSessionImage: 'Image',
boxSessionBackend: 'Backend',
boxSessionResources: 'Resources',
boxSessionNetwork: 'Network',
boxStatusLoadFailed: 'Failed to load Box status',
failedToGetDebugInfo: 'Failed to get debug information',
copiedToClipboard: 'Copied to clipboard',

View File

@@ -485,7 +485,12 @@ const esES = {
boxUnavailable: 'No disponible',
boxBackend: 'Backend',
boxProfile: 'Perfil',
boxSandboxes: 'Sandboxes',
boxErrors: 'Errores',
boxSessionImage: 'Imagen',
boxSessionBackend: 'Backend',
boxSessionResources: 'Recursos',
boxSessionNetwork: 'Red',
boxStatusLoadFailed: 'Error al cargar el estado de Box',
failedToGetDebugInfo: 'Error al obtener la información de depuración',
copiedToClipboard: 'Copiado al portapapeles',

View File

@@ -478,7 +478,12 @@ const jaJP = {
boxUnavailable: '利用不可',
boxBackend: 'バックエンド',
boxProfile: 'プロファイル',
boxSandboxes: 'サンドボックス',
boxErrors: 'エラー',
boxSessionImage: 'イメージ',
boxSessionBackend: 'バックエンド',
boxSessionResources: 'リソース',
boxSessionNetwork: 'ネットワーク',
boxStatusLoadFailed: 'Box ステータスの読み込みに失敗しました',
failedToGetDebugInfo: 'デバッグ情報の取得に失敗しました',
copiedToClipboard: 'クリップボードにコピーしました',

View File

@@ -481,7 +481,12 @@ const ruRU = {
boxUnavailable: 'Недоступно',
boxBackend: 'Бэкенд',
boxProfile: 'Профиль',
boxSandboxes: 'Песочницы',
boxErrors: 'Ошибки',
boxSessionImage: 'Образ',
boxSessionBackend: 'Бэкенд',
boxSessionResources: 'Ресурсы',
boxSessionNetwork: 'Сеть',
boxStatusLoadFailed: 'Не удалось загрузить статус Box',
failedToGetDebugInfo: 'Не удалось получить отладочную информацию',
copiedToClipboard: 'Скопировано в буфер обмена',

View File

@@ -468,7 +468,12 @@ const thTH = {
boxUnavailable: 'ไม่พร้อมใช้งาน',
boxBackend: 'แบ็กเอนด์',
boxProfile: 'โปรไฟล์',
boxSandboxes: 'แซนด์บ็อกซ์',
boxErrors: 'ข้อผิดพลาด',
boxSessionImage: 'อิมเมจ',
boxSessionBackend: 'แบ็กเอนด์',
boxSessionResources: 'ทรัพยากร',
boxSessionNetwork: 'เครือข่าย',
boxStatusLoadFailed: 'โหลดสถานะ Box ล้มเหลว',
failedToGetDebugInfo: 'ไม่สามารถดึงข้อมูลดีบักได้',
copiedToClipboard: 'คัดลอกไปยังคลิปบอร์ดแล้ว',

View File

@@ -478,7 +478,12 @@ const viVN = {
boxUnavailable: 'Không khả dụng',
boxBackend: 'Backend',
boxProfile: 'Hồ sơ',
boxSandboxes: 'Sandbox',
boxErrors: 'Lỗi',
boxSessionImage: 'Image',
boxSessionBackend: 'Backend',
boxSessionResources: 'Tài nguyên',
boxSessionNetwork: 'Mạng',
boxStatusLoadFailed: 'Tải trạng thái Box thất bại',
failedToGetDebugInfo: 'Lấy thông tin gỡ lỗi thất bại',
copiedToClipboard: 'Đã sao chép vào clipboard',

View File

@@ -452,7 +452,12 @@ const zhHans = {
boxUnavailable: '不可用',
boxBackend: '后端',
boxProfile: '配置',
boxSandboxes: '沙箱数',
boxErrors: '错误数',
boxSessionImage: '镜像',
boxSessionBackend: '后端',
boxSessionResources: '资源',
boxSessionNetwork: '网络',
boxStatusLoadFailed: '加载 Box 状态失败',
failedToGetDebugInfo: '获取调试信息失败',
copiedToClipboard: '已复制到剪贴板',

View File

@@ -453,7 +453,12 @@ const zhHant = {
boxUnavailable: '不可用',
boxBackend: '後端',
boxProfile: '設定檔',
boxSandboxes: '沙箱數',
boxErrors: '錯誤數',
boxSessionImage: '映像檔',
boxSessionBackend: '後端',
boxSessionResources: '資源',
boxSessionNetwork: '網路',
boxStatusLoadFailed: '載入 Box 狀態失敗',
failedToGetDebugInfo: '取得偵錯資訊失敗',
copiedToClipboard: '已複製到剪貼簿',