mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
Fix/storage retention cleanup (#2159)
* fix: add storage retention cleanup * fix: prune completed tasks on completion * fix: complete storage analysis i18n
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
|
||||
import i18n from '@/i18n';
|
||||
import { HardDrive } from 'lucide-react';
|
||||
|
||||
const t = (key: string) => {
|
||||
return i18n.t(key);
|
||||
@@ -51,6 +52,18 @@ export const sidebarConfigList = [
|
||||
},
|
||||
section: 'home',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'storage-analysis',
|
||||
name: t('storageAnalysis.title'),
|
||||
icon: <HardDrive className="text-blue-500" />,
|
||||
route: '/home/storage-analysis',
|
||||
description: t('storageAnalysis.description'),
|
||||
helpLink: {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
section: 'home',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'bots',
|
||||
name: t('bots.title'),
|
||||
|
||||
297
web/src/app/home/storage-analysis/page.tsx
Normal file
297
web/src/app/home/storage-analysis/page.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshCw, HardDrive, Database, FileWarning } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
|
||||
interface StorageSection {
|
||||
key: string;
|
||||
path: string;
|
||||
exists: boolean;
|
||||
size_bytes: number;
|
||||
file_count: number;
|
||||
}
|
||||
|
||||
interface CleanupCandidate {
|
||||
key?: string;
|
||||
name?: string;
|
||||
size_bytes: number;
|
||||
}
|
||||
|
||||
interface StorageAnalysis {
|
||||
generated_at: string;
|
||||
cleanup_policy: {
|
||||
uploaded_file_retention_days: number;
|
||||
log_retention_days: number;
|
||||
};
|
||||
sections: StorageSection[];
|
||||
database: {
|
||||
type: string;
|
||||
monitoring_counts: Record<string, number>;
|
||||
binary_storage: {
|
||||
count: number;
|
||||
size_bytes: number | null;
|
||||
};
|
||||
};
|
||||
cleanup_candidates: {
|
||||
uploaded_files: CleanupCandidate[];
|
||||
log_files: CleanupCandidate[];
|
||||
};
|
||||
tasks: {
|
||||
total?: number;
|
||||
running?: number;
|
||||
completed?: number;
|
||||
};
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number | null | undefined): string {
|
||||
if (bytes === null || bytes === undefined) {
|
||||
return '-';
|
||||
}
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
const units = ['KB', 'MB', 'GB', 'TB'];
|
||||
let value = bytes / 1024;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
export default function StorageAnalysisPage() {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(true);
|
||||
const [analysis, setAnalysis] = useState<StorageAnalysis | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadAnalysis = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await backendClient.get<StorageAnalysis>(
|
||||
'/api/v1/system/storage-analysis',
|
||||
);
|
||||
setAnalysis(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAnalysis();
|
||||
}, [loadAnalysis]);
|
||||
|
||||
const totalBytes = useMemo(() => {
|
||||
return (
|
||||
analysis?.sections.reduce((sum, item) => sum + item.size_bytes, 0) ?? 0
|
||||
);
|
||||
}, [analysis]);
|
||||
|
||||
const uploadedCandidateBytes = useMemo(() => {
|
||||
return (
|
||||
analysis?.cleanup_candidates.uploaded_files.reduce(
|
||||
(sum, item) => sum + item.size_bytes,
|
||||
0,
|
||||
) ?? 0
|
||||
);
|
||||
}, [analysis]);
|
||||
|
||||
const logCandidateBytes = useMemo(() => {
|
||||
return (
|
||||
analysis?.cleanup_candidates.log_files.reduce(
|
||||
(sum, item) => sum + item.size_bytes,
|
||||
0,
|
||||
) ?? 0
|
||||
);
|
||||
}, [analysis]);
|
||||
|
||||
return (
|
||||
<div className="h-full px-6 py-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">
|
||||
{t('storageAnalysis.title')}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('storageAnalysis.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="outline">
|
||||
<HardDrive className="mr-2 size-4" />
|
||||
{t('storageAnalysis.openDialog')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[82vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<HardDrive className="size-5 text-blue-500" />
|
||||
{t('storageAnalysis.dialogTitle')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{analysis
|
||||
? t('storageAnalysis.generatedAt', {
|
||||
time: new Date(analysis.generated_at).toLocaleString(),
|
||||
})
|
||||
: t('storageAnalysis.loading')}
|
||||
</div>
|
||||
<Button
|
||||
onClick={loadAnalysis}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-2 size-4 ${loading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
{t('storageAnalysis.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analysis && (
|
||||
<div className="space-y-5">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
<SummaryItem
|
||||
label={t('storageAnalysis.totalSize')}
|
||||
value={formatBytes(totalBytes)}
|
||||
icon={<HardDrive className="size-4" />}
|
||||
/>
|
||||
<SummaryItem
|
||||
label={t('storageAnalysis.binaryStorage')}
|
||||
value={formatBytes(
|
||||
analysis.database.binary_storage.size_bytes,
|
||||
)}
|
||||
icon={<Database className="size-4" />}
|
||||
/>
|
||||
<SummaryItem
|
||||
label={t('storageAnalysis.uploadCleanup')}
|
||||
value={formatBytes(uploadedCandidateBytes)}
|
||||
icon={<FileWarning className="size-4" />}
|
||||
/>
|
||||
<SummaryItem
|
||||
label={t('storageAnalysis.logCleanup')}
|
||||
value={formatBytes(logCandidateBytes)}
|
||||
icon={<FileWarning className="size-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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] gap-3 border-b px-3 py-2 text-sm last:border-b-0"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{t(`storageAnalysis.sectionNames.${section.key}`)}
|
||||
</div>
|
||||
<div className="break-all text-xs text-muted-foreground">
|
||||
{section.path}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<section className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="mb-2 text-sm font-medium">
|
||||
{t('storageAnalysis.monitoringTables')}
|
||||
</h2>
|
||||
<KeyValueList values={analysis.database.monitoring_counts} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="mb-2 text-sm font-medium">
|
||||
{t('storageAnalysis.runtimeTasks')}
|
||||
</h2>
|
||||
<KeyValueList values={analysis.tasks} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryItem({
|
||||
label,
|
||||
value,
|
||||
icon,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-md border px-3 py-3">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-2 text-xl font-semibold tabular-nums">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyValueList({
|
||||
values,
|
||||
}: {
|
||||
values: Record<string, number | undefined>;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
{Object.entries(values).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between border-b px-3 py-2 text-sm last:border-b-0"
|
||||
>
|
||||
<span className="text-muted-foreground">{key}</span>
|
||||
<span className="font-medium tabular-nums">{value ?? '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1225,6 +1225,31 @@ const enUS = {
|
||||
feedback: 'User Feedback',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: 'Storage Analysis',
|
||||
description: 'Inspect storage usage and cleanup candidates',
|
||||
openDialog: 'View Analysis',
|
||||
dialogTitle: 'Storage Analysis',
|
||||
generatedAt: 'Generated at {{time}}',
|
||||
loading: 'Loading...',
|
||||
refresh: 'Refresh',
|
||||
totalSize: 'Total size',
|
||||
binaryStorage: 'Binary storage',
|
||||
uploadCleanup: 'Expired uploads',
|
||||
logCleanup: 'Expired logs',
|
||||
sections: 'Storage sections',
|
||||
monitoringTables: 'Monitoring tables',
|
||||
runtimeTasks: 'Runtime tasks',
|
||||
sectionNames: {
|
||||
database: 'Database',
|
||||
logs: 'Logs',
|
||||
storage: 'Uploaded files',
|
||||
vector_store: 'Vector store',
|
||||
plugins: 'Plugins',
|
||||
mcp: 'MCP',
|
||||
temp: 'Temporary files',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'Maximum number of bots ({{max}}) reached. Please remove an existing bot before creating a new one.',
|
||||
|
||||
@@ -1259,6 +1259,32 @@ const esES = {
|
||||
feedback: 'Comentarios de usuarios',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: 'Análisis de almacenamiento',
|
||||
description:
|
||||
'Inspecciona el uso de almacenamiento y los candidatos de limpieza',
|
||||
openDialog: 'Ver análisis',
|
||||
dialogTitle: 'Análisis de almacenamiento',
|
||||
generatedAt: 'Generado el {{time}}',
|
||||
loading: 'Cargando...',
|
||||
refresh: 'Actualizar',
|
||||
totalSize: 'Tamaño total',
|
||||
binaryStorage: 'Almacenamiento binario de plugins',
|
||||
uploadCleanup: 'Subidas caducadas',
|
||||
logCleanup: 'Registros caducados',
|
||||
sections: 'Secciones de almacenamiento',
|
||||
monitoringTables: 'Tablas de monitoreo',
|
||||
runtimeTasks: 'Tareas en ejecución',
|
||||
sectionNames: {
|
||||
database: 'Base de datos',
|
||||
logs: 'Registros',
|
||||
storage: 'Archivos subidos',
|
||||
vector_store: 'Almacén vectorial',
|
||||
plugins: 'Plugins',
|
||||
mcp: 'MCP',
|
||||
temp: 'Archivos temporales',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'Se ha alcanzado el número máximo de Bots ({{max}}). Por favor, elimina un Bot existente antes de crear uno nuevo.',
|
||||
|
||||
@@ -1230,6 +1230,31 @@
|
||||
feedback: 'ユーザーフィードバック',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: 'ストレージ分析',
|
||||
description: 'ストレージ使用量とクリーンアップ候補を確認します',
|
||||
openDialog: '分析を表示',
|
||||
dialogTitle: 'ストレージ分析',
|
||||
generatedAt: '生成日時 {{time}}',
|
||||
loading: '読み込み中...',
|
||||
refresh: '更新',
|
||||
totalSize: '合計サイズ',
|
||||
binaryStorage: 'プラグインバイナリストレージ',
|
||||
uploadCleanup: '期限切れアップロード',
|
||||
logCleanup: '期限切れログ',
|
||||
sections: 'ストレージセクション',
|
||||
monitoringTables: '監視テーブル',
|
||||
runtimeTasks: '実行タスク',
|
||||
sectionNames: {
|
||||
database: 'データベース',
|
||||
logs: 'ログ',
|
||||
storage: 'アップロードファイル',
|
||||
vector_store: 'ベクターストア',
|
||||
plugins: 'プラグイン',
|
||||
mcp: 'MCP',
|
||||
temp: '一時ファイル',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'ボット数が上限({{max}}個)に達しました。新しいボットを作成するには、既存のボットを削除してください。',
|
||||
|
||||
@@ -1234,6 +1234,31 @@ const ruRU = {
|
||||
feedback: 'Отзывы пользователей',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: 'Анализ хранилища',
|
||||
description: 'Проверьте использование хранилища и кандидатов на очистку',
|
||||
openDialog: 'Открыть анализ',
|
||||
dialogTitle: 'Анализ хранилища',
|
||||
generatedAt: 'Создано {{time}}',
|
||||
loading: 'Загрузка...',
|
||||
refresh: 'Обновить',
|
||||
totalSize: 'Общий размер',
|
||||
binaryStorage: 'Бинарное хранилище плагинов',
|
||||
uploadCleanup: 'Просроченные загрузки',
|
||||
logCleanup: 'Просроченные журналы',
|
||||
sections: 'Разделы хранилища',
|
||||
monitoringTables: 'Таблицы мониторинга',
|
||||
runtimeTasks: 'Задачи runtime',
|
||||
sectionNames: {
|
||||
database: 'База данных',
|
||||
logs: 'Журналы',
|
||||
storage: 'Загруженные файлы',
|
||||
vector_store: 'Векторное хранилище',
|
||||
plugins: 'Плагины',
|
||||
mcp: 'MCP',
|
||||
temp: 'Временные файлы',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'Достигнуто максимальное количество ботов ({{max}}). Удалите существующего бота перед созданием нового.',
|
||||
|
||||
@@ -1205,6 +1205,31 @@ const thTH = {
|
||||
feedback: 'ความคิดเห็นผู้ใช้',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: 'วิเคราะห์พื้นที่จัดเก็บ',
|
||||
description: 'ตรวจสอบการใช้พื้นที่จัดเก็บและรายการที่สามารถล้างได้',
|
||||
openDialog: 'ดูการวิเคราะห์',
|
||||
dialogTitle: 'วิเคราะห์พื้นที่จัดเก็บ',
|
||||
generatedAt: 'สร้างเมื่อ {{time}}',
|
||||
loading: 'กำลังโหลด...',
|
||||
refresh: 'รีเฟรช',
|
||||
totalSize: 'ขนาดรวม',
|
||||
binaryStorage: 'พื้นที่จัดเก็บไบนารีของปลั๊กอิน',
|
||||
uploadCleanup: 'ไฟล์อัปโหลดที่หมดอายุ',
|
||||
logCleanup: 'บันทึกที่หมดอายุ',
|
||||
sections: 'ส่วนพื้นที่จัดเก็บ',
|
||||
monitoringTables: 'ตารางการตรวจสอบ',
|
||||
runtimeTasks: 'งาน runtime',
|
||||
sectionNames: {
|
||||
database: 'ฐานข้อมูล',
|
||||
logs: 'บันทึก',
|
||||
storage: 'ไฟล์อัปโหลด',
|
||||
vector_store: 'คลังเวกเตอร์',
|
||||
plugins: 'ปลั๊กอิน',
|
||||
mcp: 'MCP',
|
||||
temp: 'ไฟล์ชั่วคราว',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'จำนวน Bot สูงสุด ({{max}}) ถึงขีดจำกัดแล้ว กรุณาลบ Bot ที่มีอยู่ก่อนสร้างใหม่',
|
||||
|
||||
@@ -1227,6 +1227,31 @@ const viVN = {
|
||||
feedback: 'Phản hồi người dùng',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: 'Phân tích lưu trữ',
|
||||
description: 'Kiểm tra dung lượng lưu trữ và các mục có thể dọn dẹp',
|
||||
openDialog: 'Xem phân tích',
|
||||
dialogTitle: 'Phân tích lưu trữ',
|
||||
generatedAt: 'Tạo lúc {{time}}',
|
||||
loading: 'Đang tải...',
|
||||
refresh: 'Làm mới',
|
||||
totalSize: 'Tổng dung lượng',
|
||||
binaryStorage: 'Lưu trữ nhị phân plugin',
|
||||
uploadCleanup: 'Tệp tải lên hết hạn',
|
||||
logCleanup: 'Nhật ký hết hạn',
|
||||
sections: 'Khu vực lưu trữ',
|
||||
monitoringTables: 'Bảng giám sát',
|
||||
runtimeTasks: 'Tác vụ runtime',
|
||||
sectionNames: {
|
||||
database: 'Cơ sở dữ liệu',
|
||||
logs: 'Nhật ký',
|
||||
storage: 'Tệp tải lên',
|
||||
vector_store: 'Kho vector',
|
||||
plugins: 'Plugin',
|
||||
mcp: 'MCP',
|
||||
temp: 'Tệp tạm',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'Đã đạt số lượng Bot tối đa ({{max}}). Vui lòng xóa một Bot hiện có trước khi tạo mới.',
|
||||
|
||||
@@ -1171,6 +1171,31 @@ const zhHans = {
|
||||
feedback: '用户反馈',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: '存储分析',
|
||||
description: '查看存储占用和可清理文件',
|
||||
openDialog: '查看分析',
|
||||
dialogTitle: '存储分析',
|
||||
generatedAt: '生成时间 {{time}}',
|
||||
loading: '加载中...',
|
||||
refresh: '刷新',
|
||||
totalSize: '总占用',
|
||||
binaryStorage: '插件二进制存储',
|
||||
uploadCleanup: '过期上传文件',
|
||||
logCleanup: '过期日志',
|
||||
sections: '存储分区',
|
||||
monitoringTables: '监控表',
|
||||
runtimeTasks: '运行任务',
|
||||
sectionNames: {
|
||||
database: '数据库',
|
||||
logs: '日志',
|
||||
storage: '上传文件',
|
||||
vector_store: '向量库',
|
||||
plugins: '插件',
|
||||
mcp: 'MCP',
|
||||
temp: '临时文件',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'已达到机器人数量上限({{max}}个)。请先删除已有机器人后再创建新的。',
|
||||
|
||||
@@ -1171,6 +1171,31 @@ const zhHant = {
|
||||
feedback: '使用者回饋',
|
||||
},
|
||||
},
|
||||
storageAnalysis: {
|
||||
title: '儲存分析',
|
||||
description: '查看儲存占用和可清理檔案',
|
||||
openDialog: '查看分析',
|
||||
dialogTitle: '儲存分析',
|
||||
generatedAt: '生成時間 {{time}}',
|
||||
loading: '載入中...',
|
||||
refresh: '重新整理',
|
||||
totalSize: '總占用',
|
||||
binaryStorage: '插件二進位儲存',
|
||||
uploadCleanup: '過期上傳檔案',
|
||||
logCleanup: '過期日誌',
|
||||
sections: '儲存分區',
|
||||
monitoringTables: '監控表',
|
||||
runtimeTasks: '執行任務',
|
||||
sectionNames: {
|
||||
database: '資料庫',
|
||||
logs: '日誌',
|
||||
storage: '上傳檔案',
|
||||
vector_store: '向量庫',
|
||||
plugins: '插件',
|
||||
mcp: 'MCP',
|
||||
temp: '暫存檔案',
|
||||
},
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
'已達到機器人數量上限({{max}}個)。請先刪除已有機器人後再建立新的。',
|
||||
|
||||
Reference in New Issue
Block a user