Files
beaver_project/app-instance/frontend/app/(app)/status/page.tsx
steven_li bfa77204bf feat(server): 添加系统重启功能支持
添加后台任务支持用于处理系统重启请求,实现延迟进程终止机制,
允许系统安全地重启并在重启后自动恢复服务。

feat(frontend): 实现前端系统重启控制面板

在状态页面添加重启对话框组件,提供用户友好的重启确认界面,
包含重启状态监控和错误处理功能,确保用户可以安全重启系统。

docs(deployment): 更新部署指南添加URL协议要求说明

详细说明NANO_AUTHZ_URL和NANO_DEPLOY_URL环境变量必须包含
http://协议前缀的要求,添加常见错误示例和容器重建步骤,
帮助用户避免注册页面502错误问题。
2026-03-19 10:26:43 +08:00

309 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useEffect, useState } from 'react';
import {
CheckCircle2,
XCircle,
AlertCircle,
RefreshCw,
Server,
Cpu,
Radio,
Key,
Loader2,
} from 'lucide-react';
import { getStatus, restartSystem } from '@/lib/api';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import type { SystemStatus } from '@/types';
export default function StatusPage() {
const [status, setStatus] = useState<SystemStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
const [restarting, setRestarting] = useState(false);
const [restartError, setRestartError] = useState<string | null>(null);
const loadStatus = async () => {
setLoading(true);
setError(null);
try {
const data = await getStatus();
setStatus(data);
} catch (err: any) {
setError(err.message || '连接后端失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadStatus();
}, []);
useEffect(() => {
if (!restarting) {
return;
}
const intervalId = window.setInterval(async () => {
try {
await getStatus();
window.location.reload();
} catch {
// Ignore failures until the container is back.
}
}, 3000);
return () => {
window.clearInterval(intervalId);
};
}, [restarting]);
const handleRestart = async () => {
setRestartError(null);
try {
await restartSystem();
setRestartDialogOpen(false);
setRestarting(true);
} catch (err: any) {
setRestartError(err.message || '重启失败');
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
if (error) {
return (
<div className="max-w-4xl mx-auto p-6">
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="flex items-center gap-3 text-destructive">
<AlertCircle className="w-5 h-5" />
<div>
<p className="font-medium"> Boardware Agent Sandbox </p>
<p className="text-sm text-muted-foreground mt-1">{error}</p>
<p className="text-sm text-muted-foreground mt-1">
访
</p>
</div>
</div>
<Button onClick={loadStatus} variant="outline" size="sm" className="mt-4">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
</div>
);
}
if (!status) return null;
return (
<div className="max-w-4xl mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<Button onClick={loadStatus} variant="outline" size="sm" disabled={restarting}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
{/* System Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Server className="w-4 h-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<p className="text-sm font-medium"></p>
<p className="text-sm text-muted-foreground">
{restarting
? '正在重启当前 docker服务恢复后页面会自动刷新。'
: '会重启当前 docker 容器。重启完成后需要重新登录。'}
</p>
{restartError ? (
<p className="text-sm text-destructive">{restartError}</p>
) : null}
</div>
<AlertDialog open={restartDialogOpen} onOpenChange={setRestartDialogOpen}>
<Button
variant="destructive"
onClick={() => setRestartDialogOpen(true)}
disabled={restarting}
>
{restarting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<RefreshCw className="w-4 h-4 mr-2" />
)}
Restart
</Button>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
docker
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={restarting}></AlertDialogCancel>
<AlertDialogAction onClick={handleRestart} disabled={restarting}>
{restarting ? '重启中...' : '确认 Restart'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
{/* Model Config */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Cpu className="w-4 h-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<InfoRow label="模型" value={status.model} />
<InfoRow label="最大令牌数" value={String(status.max_tokens)} />
<InfoRow label="温度" value={String(status.temperature)} />
<InfoRow
label="最大工具迭代次数"
value={String(status.max_tool_iterations)}
/>
</CardContent>
</Card>
{/* Providers */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Key className="w-4 h-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{status.providers.map((p) => (
<div
key={p.name}
className="flex items-center gap-2 text-sm"
>
{p.has_key ? (
<CheckCircle2 className="w-4 h-4 text-green-500" />
) : (
<XCircle className="w-4 h-4 text-muted-foreground/40" />
)}
<span className={p.has_key ? '' : 'text-muted-foreground'}>
{p.name}
</span>
{p.detail && (
<span className="text-xs text-muted-foreground truncate">
{p.detail}
</span>
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* Channels */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Radio className="w-4 h-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{status.channels.map((ch) => (
<div key={ch.name} className="flex items-center gap-2 text-sm">
<Badge
variant={ch.enabled ? 'default' : 'secondary'}
className="text-xs"
>
{ch.enabled ? '开启' : '关闭'}
</Badge>
<span className="capitalize">{ch.name}</span>
</div>
))}
</div>
</CardContent>
</Card>
{/* Cron Summary */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<AlertCircle className="w-4 h-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<InfoRow
label="状态"
value={status.cron.enabled ? '运行中' : '已停止'}
ok={status.cron.enabled}
/>
<InfoRow label="任务数" value={String(status.cron.jobs)} />
</CardContent>
</Card>
</div>
);
}
function InfoRow({
label,
value,
ok,
}: {
label: string;
value: string;
ok?: boolean;
}) {
return (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{label}</span>
<div className="flex items-center gap-2">
<code className="bg-muted px-2 py-0.5 rounded text-xs max-w-[400px] truncate">
{value}
</code>
{ok !== undefined &&
(ok ? (
<CheckCircle2 className="w-4 h-4 text-green-500" />
) : (
<XCircle className="w-4 h-4 text-destructive" />
))}
</div>
</div>
);
}