mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(storage): group usage by application and runtimes
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { PanelToolbar } from '../settings-dialog/panel-layout';
|
||||
|
||||
@@ -31,6 +32,24 @@ interface StorageSection {
|
||||
file_count: number;
|
||||
}
|
||||
|
||||
interface RuntimeStorageDirectory extends StorageSection {
|
||||
kind: 'root' | 'detail';
|
||||
parent_key?: string;
|
||||
error_count?: number;
|
||||
scope?: 'runtime_host' | 'sandbox_sessions';
|
||||
}
|
||||
|
||||
interface RuntimeStorageProcess {
|
||||
key: 'langbot' | 'plugin_runtime' | 'box_runtime';
|
||||
status: 'available' | 'unavailable' | 'disabled' | 'not_applicable';
|
||||
source: 'local_process' | 'runtime_rpc';
|
||||
size_bytes: number | null;
|
||||
directories: RuntimeStorageDirectory[];
|
||||
error?: string;
|
||||
active_sessions?: number | null;
|
||||
managed_processes?: number | null;
|
||||
}
|
||||
|
||||
interface CleanupCandidate {
|
||||
key?: string;
|
||||
name?: string;
|
||||
@@ -46,6 +65,8 @@ interface StorageAnalysis {
|
||||
log_retention_days: number;
|
||||
};
|
||||
sections: StorageSection[];
|
||||
processes?: RuntimeStorageProcess[];
|
||||
total_size_bytes?: number;
|
||||
database: {
|
||||
type: string;
|
||||
monitoring_counts: Record<string, number>;
|
||||
@@ -114,7 +135,9 @@ export default function StorageAnalysisPanel({
|
||||
|
||||
const totalBytes = useMemo(() => {
|
||||
return (
|
||||
analysis?.sections.reduce((sum, item) => sum + item.size_bytes, 0) ?? 0
|
||||
analysis?.total_size_bytes ??
|
||||
analysis?.sections.reduce((sum, item) => sum + item.size_bytes, 0) ??
|
||||
0
|
||||
);
|
||||
}, [analysis]);
|
||||
|
||||
@@ -168,6 +191,8 @@ export default function StorageAnalysisPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && !analysis && <StorageAnalysisSkeleton />}
|
||||
|
||||
{analysis && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
@@ -219,41 +244,28 @@ export default function StorageAnalysisPanel({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-2 text-sm font-medium">
|
||||
{t('storageAnalysis.sections')}
|
||||
</h2>
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
{analysis.sections.map((section) => (
|
||||
<div
|
||||
key={section.key}
|
||||
className="grid grid-cols-[1fr_auto_auto_auto] gap-3 border-b px-3 py-2 text-sm last:border-b-0"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium">
|
||||
{t(`storageAnalysis.sectionNames.${section.key}`)}
|
||||
</div>
|
||||
<div className="break-all text-xs text-muted-foreground">
|
||||
{section.path || '-'}
|
||||
</div>
|
||||
</div>
|
||||
{section.exists ? (
|
||||
<span />
|
||||
) : (
|
||||
<Badge variant="outline" className="self-center">
|
||||
{t('storageAnalysis.missing')}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="self-center tabular-nums">
|
||||
{formatBytes(section.size_bytes)}
|
||||
</div>
|
||||
<div className="self-center text-muted-foreground tabular-nums">
|
||||
{section.file_count}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{analysis.processes?.length ? (
|
||||
<section>
|
||||
<div className="mb-3">
|
||||
<h2 className="text-sm font-medium">
|
||||
{t('storageAnalysis.processStorage')}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t('storageAnalysis.processStorageDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{analysis.processes.map((process) => (
|
||||
<RuntimeStorageGroup
|
||||
key={process.key}
|
||||
process={process}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<LegacyStorageSections sections={analysis.sections} />
|
||||
)}
|
||||
|
||||
<section className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<MetricPanel
|
||||
@@ -286,6 +298,171 @@ export default function StorageAnalysisPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function StorageAnalysisSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4" aria-hidden="true">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<Skeleton key={index} className="h-24 rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="space-y-3 rounded-md border p-4">
|
||||
<Skeleton className="h-5 w-40" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimeStorageGroup({ process }: { process: RuntimeStorageProcess }) {
|
||||
const { t } = useTranslation();
|
||||
const available = process.status === 'available';
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border bg-card">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 border-b bg-muted/20 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t(`storageAnalysis.processNames.${process.key}`)}
|
||||
</h3>
|
||||
<Badge variant={available ? 'secondary' : 'outline'}>
|
||||
{t(`storageAnalysis.statusLabels.${process.status}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t(`storageAnalysis.processDescriptions.${process.key}`)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t(`storageAnalysis.sourceLabels.${process.source}`)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-base font-semibold tabular-nums">
|
||||
{formatBytes(process.size_bytes)}
|
||||
</div>
|
||||
{process.key === 'box_runtime' && available && (
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{t('storageAnalysis.boxActivity', {
|
||||
sessions: process.active_sessions ?? 0,
|
||||
processes: process.managed_processes ?? 0,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!available ? (
|
||||
<div className="px-4 py-4 text-sm text-muted-foreground">
|
||||
{process.error || t('storageAnalysis.runtimeUnavailable')}
|
||||
</div>
|
||||
) : process.directories.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
{t('storageAnalysis.noManagedDirectories')}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="hidden grid-cols-[minmax(0,1fr)_100px_90px] gap-4 border-b px-4 py-2 text-xs text-muted-foreground sm:grid">
|
||||
<span>{t('storageAnalysis.directory')}</span>
|
||||
<span className="text-right">{t('storageAnalysis.size')}</span>
|
||||
<span className="text-right">{t('storageAnalysis.files')}</span>
|
||||
</div>
|
||||
{process.directories.map((directory) => (
|
||||
<div
|
||||
key={directory.key}
|
||||
className="grid grid-cols-[minmax(0,1fr)_auto] gap-3 border-b px-4 py-3 text-sm last:border-b-0 sm:grid-cols-[minmax(0,1fr)_100px_90px] sm:gap-4"
|
||||
>
|
||||
<div
|
||||
className={`min-w-0 ${
|
||||
directory.kind === 'detail'
|
||||
? 'ml-3 border-l-2 border-muted pl-3'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">
|
||||
{t(`storageAnalysis.sectionNames.${directory.key}`)}
|
||||
</span>
|
||||
{!directory.exists && (
|
||||
<Badge variant="outline" className="font-normal">
|
||||
{t('storageAnalysis.notCreated')}
|
||||
</Badge>
|
||||
)}
|
||||
{!!directory.error_count && (
|
||||
<Badge variant="outline" className="font-normal">
|
||||
{t('storageAnalysis.scanWarnings', {
|
||||
count: directory.error_count,
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
{directory.scope && (
|
||||
<Badge variant="outline" className="font-normal">
|
||||
{t(`storageAnalysis.scopeLabels.${directory.scope}`)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 break-all font-mono text-xs text-muted-foreground">
|
||||
{directory.path || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center text-right tabular-nums">
|
||||
{formatBytes(directory.size_bytes)}
|
||||
</div>
|
||||
<div className="col-span-2 self-center text-right text-xs text-muted-foreground tabular-nums sm:col-span-1 sm:text-sm">
|
||||
{directory.file_count}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LegacyStorageSections({ sections }: { sections: StorageSection[] }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-2 text-sm font-medium">
|
||||
{t('storageAnalysis.sections')}
|
||||
</h2>
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
{sections.map((section) => (
|
||||
<div
|
||||
key={section.key}
|
||||
className="grid grid-cols-[1fr_auto_auto_auto] gap-3 border-b px-3 py-2 text-sm last:border-b-0"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium">
|
||||
{t(`storageAnalysis.sectionNames.${section.key}`)}
|
||||
</div>
|
||||
<div className="break-all text-xs text-muted-foreground">
|
||||
{section.path || '-'}
|
||||
</div>
|
||||
</div>
|
||||
{section.exists ? (
|
||||
<span />
|
||||
) : (
|
||||
<Badge variant="outline" className="self-center">
|
||||
{t('storageAnalysis.missing')}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="self-center tabular-nums">
|
||||
{formatBytes(section.size_bytes)}
|
||||
</div>
|
||||
<div className="self-center text-muted-foreground tabular-nums">
|
||||
{section.file_count}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryItem({
|
||||
label,
|
||||
value,
|
||||
|
||||
@@ -2050,6 +2050,44 @@ const enUS = {
|
||||
databaseType: 'Database type',
|
||||
days: 'days',
|
||||
missing: 'Missing',
|
||||
notCreated: 'Not created yet',
|
||||
processStorage: 'Storage by process',
|
||||
processStorageDescription:
|
||||
'Each runtime measures the directories it owns. Detail rows are included in their parent directory total.',
|
||||
directory: 'Directory',
|
||||
size: 'Size',
|
||||
files: 'Files',
|
||||
runtimeUnavailable: 'Runtime storage statistics are unavailable.',
|
||||
noManagedDirectories: 'No managed directories were reported.',
|
||||
scanWarnings: '{{count}} unreadable entries',
|
||||
boxActivity: '{{sessions}} sessions · {{processes}} managed processes',
|
||||
statusLabels: {
|
||||
available: 'Available',
|
||||
unavailable: 'Unavailable',
|
||||
disabled: 'Disabled',
|
||||
not_applicable: 'Not applicable',
|
||||
},
|
||||
sourceLabels: {
|
||||
local_process: 'Measured by the LangBot process',
|
||||
runtime_rpc: 'Measured by the runtime process over authenticated RPC',
|
||||
},
|
||||
scopeLabels: {
|
||||
runtime_host: 'Runtime host',
|
||||
sandbox_sessions: 'Sandbox sessions',
|
||||
},
|
||||
processNames: {
|
||||
langbot: 'LangBot main process',
|
||||
plugin_runtime: 'Plugin Runtime',
|
||||
box_runtime: 'Box Runtime',
|
||||
},
|
||||
processDescriptions: {
|
||||
langbot:
|
||||
'Application database, logs, uploaded files, vector data and temporary files.',
|
||||
plugin_runtime:
|
||||
'Plugin packages, verified artifacts, dependency environments and private installation data.',
|
||||
box_runtime:
|
||||
'Sandbox workspaces, MCP process workspaces, attachment exchange directories and skills.',
|
||||
},
|
||||
expiredUploads: 'Expired uploads',
|
||||
expiredLogs: 'Expired logs',
|
||||
noExpiredUploads: 'No expired uploaded files',
|
||||
@@ -2062,6 +2100,20 @@ const enUS = {
|
||||
plugins: 'Plugins',
|
||||
mcp: 'MCP',
|
||||
temp: 'Temporary files',
|
||||
legacy_plugins: 'Legacy plugin packages',
|
||||
artifacts: 'Verified plugin artifacts',
|
||||
dependency_environments: 'Plugin dependency environments',
|
||||
installations: 'Plugin installation data',
|
||||
staging: 'Plugin staging files',
|
||||
rpc_transfer: 'Runtime RPC transfer files',
|
||||
workspace: 'Sandbox workspace',
|
||||
inbox: 'Inbound attachments',
|
||||
outbox: 'Outbound attachments',
|
||||
skills: 'Skills',
|
||||
session_workspaces: 'Sandbox session workspaces',
|
||||
session_caches: 'Sandbox runtime caches',
|
||||
session_temp: 'Sandbox temporary files',
|
||||
managed_process_workspaces: 'Managed-process workspaces (including MCP)',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
|
||||
@@ -1958,6 +1958,41 @@ const zhHans = {
|
||||
databaseType: '数据库类型',
|
||||
days: '天',
|
||||
missing: '不存在',
|
||||
notCreated: '尚未创建',
|
||||
processStorage: '各进程管理的存储',
|
||||
processStorageDescription:
|
||||
'由每个运行时直接统计其负责的目录;明细目录已包含在上级目录的总占用中。',
|
||||
directory: '目录',
|
||||
size: '占用',
|
||||
files: '文件数',
|
||||
runtimeUnavailable: '暂时无法获取该运行时的存储统计。',
|
||||
noManagedDirectories: '该进程未报告任何受管目录。',
|
||||
scanWarnings: '{{count}} 项无法读取',
|
||||
boxActivity: '{{sessions}} 个会话 · {{processes}} 个受管进程',
|
||||
statusLabels: {
|
||||
available: '统计正常',
|
||||
unavailable: '运行时未连接',
|
||||
disabled: '未启用',
|
||||
not_applicable: '不适用',
|
||||
},
|
||||
sourceLabels: {
|
||||
local_process: '由 LangBot 主进程直接统计',
|
||||
runtime_rpc: '由运行时进程通过鉴权 RPC 直接统计',
|
||||
},
|
||||
scopeLabels: {
|
||||
runtime_host: '运行时宿主目录',
|
||||
sandbox_sessions: '沙箱会话内部',
|
||||
},
|
||||
processNames: {
|
||||
langbot: 'LangBot 主程序',
|
||||
plugin_runtime: 'Plugin Runtime',
|
||||
box_runtime: 'Box Runtime',
|
||||
},
|
||||
processDescriptions: {
|
||||
langbot: '应用数据库、日志、上传文件、向量数据和主程序临时文件。',
|
||||
plugin_runtime: '插件包、校验后的制品、依赖环境和安装实例私有数据。',
|
||||
box_runtime: '沙箱工作区、MCP 进程工作区、附件交换目录和技能目录。',
|
||||
},
|
||||
expiredUploads: '过期上传文件',
|
||||
expiredLogs: '过期日志',
|
||||
noExpiredUploads: '暂无过期上传文件',
|
||||
@@ -1970,6 +2005,20 @@ const zhHans = {
|
||||
plugins: '插件',
|
||||
mcp: 'MCP',
|
||||
temp: '临时文件',
|
||||
legacy_plugins: '旧版插件包',
|
||||
artifacts: '已校验插件制品',
|
||||
dependency_environments: '插件依赖环境',
|
||||
installations: '插件安装实例数据',
|
||||
staging: '插件安装暂存区',
|
||||
rpc_transfer: '运行时 RPC 传输文件',
|
||||
workspace: '沙箱工作区',
|
||||
inbox: '输入附件',
|
||||
outbox: '输出附件',
|
||||
skills: '技能目录',
|
||||
session_workspaces: '沙箱会话工作区',
|
||||
session_caches: '沙箱运行时缓存',
|
||||
session_temp: '沙箱临时文件',
|
||||
managed_process_workspaces: '受管进程工作区(包括 MCP)',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
|
||||
Reference in New Issue
Block a user