Refine webui layout and config defaults

This commit is contained in:
lpf
2026-03-07 14:23:48 +08:00
parent fe0437b9cf
commit b5bb9a33e7
23 changed files with 1132 additions and 357 deletions

View File

@@ -1,92 +1,182 @@
import React, { useMemo } from 'react';
import { RefreshCw, Activity, MessageSquare, Clock, Server, CheckCircle2, Pause } from 'lucide-react';
import { RefreshCw, Activity, MessageSquare, Clock, Server, Wrench, Sparkles, AlertTriangle, Workflow } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import StatCard from '../components/StatCard';
const Dashboard: React.FC = () => {
const { t } = useTranslation();
const { isGatewayOnline, sessions, cron, nodes, refreshAll, gatewayVersion, webuiVersion } = useAppContext();
const {
isGatewayOnline,
sessions,
cron,
nodes,
refreshAll,
gatewayVersion,
webuiVersion,
skills,
cfg,
taskQueueItems,
ekgSummary,
} = useAppContext();
const onlineNodes = useMemo(() => {
const parsedNodes = useMemo(() => {
try {
const arr = JSON.parse(nodes);
return Array.isArray(arr) ? arr.filter((n: any) => n?.online).length : 0;
return Array.isArray(arr) ? arr : [];
} catch {
return 0;
return [];
}
}, [nodes]);
const onlineNodes = useMemo(
() => parsedNodes.filter((n: any) => n?.online).length,
[parsedNodes],
);
const pausedCron = useMemo(
() => cron.filter((job) => !job.enabled).length,
[cron],
);
const enabledCron = useMemo(
() => cron.filter((job) => job.enabled).length,
[cron],
);
const subagentCount = useMemo(() => {
const subagents = (cfg as any)?.agents?.subagents || {};
return Object.keys(subagents).length;
}, [cfg]);
const recentTasks = useMemo(() => {
return [...taskQueueItems]
.sort((a: any, b: any) => String(b.time || '').localeCompare(String(a.time || '')))
.slice(0, 8);
}, [taskQueueItems]);
const recentFailures = useMemo(() => {
return recentTasks.filter((item: any) => String(item.status || '').toLowerCase() === 'error').slice(0, 5);
}, [recentTasks]);
const ekgEscalationCount = Number(ekgSummary?.escalation_count || 0);
const ekgTopProvider = (Array.isArray(ekgSummary?.provider_top_workload) ? ekgSummary.provider_top_workload[0]?.key : '') || '-';
const ekgTopErrSig = (Array.isArray(ekgSummary?.errsig_top_workload) ? ekgSummary.errsig_top_workload[0]?.key : '') || '-';
return (
<div className="p-4 md:p-8 max-w-7xl mx-auto space-y-5 md:space-y-8">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="p-4 md:p-6 xl:p-8 w-full space-y-6 xl:space-y-8">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-xl md:text-2xl font-semibold tracking-tight">{t('dashboard')}</h1>
<div className="mt-1 text-xs text-zinc-500">{t('gateway')}: {gatewayVersion} · {t('webui')}: {webuiVersion}</div>
<h1 className="text-2xl font-semibold tracking-tight">{t('dashboard')}</h1>
<div className="mt-2 text-sm text-zinc-500">
{t('gateway')}: <span className="font-mono text-zinc-300">{gatewayVersion}</span>
{' · '}
{t('webui')}: <span className="font-mono text-zinc-300">{webuiVersion}</span>
</div>
</div>
<button onClick={refreshAll} className="flex items-center gap-2 px-3 md:px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors shrink-0">
<button onClick={refreshAll} className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors shrink-0">
<RefreshCw className="w-4 h-4" /> {t('refreshAll')}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 2xl:grid-cols-6 gap-4">
<StatCard title={t('gatewayStatus')} value={isGatewayOnline ? t('online') : t('offline')} icon={<Activity className={`w-6 h-6 ${isGatewayOnline ? 'text-emerald-400' : 'text-red-400'}`} />} />
<StatCard title={t('activeSessions')} value={sessions.length} icon={<MessageSquare className="w-6 h-6 text-blue-400" />} />
<StatCard title={t('cronJobs')} value={cron.length} icon={<Clock className="w-6 h-6 text-purple-400" />} />
<StatCard title={t('nodesOnline')} value={onlineNodes} icon={<Server className="w-6 h-6 text-amber-400" />} />
<StatCard title={t('cronJobs')} value={cron.length} icon={<Clock className="w-6 h-6 text-purple-400" />} />
<StatCard title={t('skills')} value={skills.length} icon={<Sparkles className="w-6 h-6 text-pink-400" />} />
<StatCard title={t('subagentsRuntime')} value={subagentCount} icon={<Wrench className="w-6 h-6 text-cyan-400" />} />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-6">
<h2 className="text-lg font-medium mb-4 flex items-center gap-2 text-zinc-200">
<Clock className="w-5 h-5 text-zinc-400"/> {t('recentCron')}
</h2>
<div className="space-y-3">
{cron.slice(0, 5).map(j => (
<div key={j.id} className="flex items-center justify-between p-3 bg-zinc-950/50 rounded-xl border border-zinc-800/50">
<span className="font-medium text-sm text-zinc-300">{j.name || j.id}</span>
{j.enabled ? (
<span className="flex items-center gap-1.5 text-xs font-medium text-emerald-400 bg-emerald-400/10 px-2.5 py-1 rounded-full">
<CheckCircle2 className="w-3.5 h-3.5"/> {t('active')}
</span>
) : (
<span className="flex items-center gap-1.5 text-xs font-medium text-zinc-400 bg-zinc-800 px-2.5 py-1 rounded-full">
<Pause className="w-3.5 h-3.5"/> {t('paused')}
</span>
)}
</div>
))}
{cron.length === 0 && <div className="text-sm text-zinc-500 text-center py-8">{t('noCronJobs')}</div>}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="rounded-2xl border border-zinc-800 bg-zinc-900/40 p-5 min-h-[148px]">
<div className="flex items-center gap-2 text-zinc-200 mb-2">
<AlertTriangle className="w-4 h-4 text-amber-400" />
<div className="text-sm font-medium">{t('ekgEscalations')}</div>
</div>
<div className="text-3xl font-semibold text-zinc-100">{ekgEscalationCount}</div>
<div className="mt-2 text-xs text-zinc-500">{t('dashboardTopErrorSignature')}: {ekgTopErrSig}</div>
</div>
<div className="rounded-2xl border border-zinc-800 bg-zinc-900/40 p-5 min-h-[148px]">
<div className="flex items-center gap-2 text-zinc-200 mb-2">
<Workflow className="w-4 h-4 text-sky-400" />
<div className="text-sm font-medium">{t('ekgTopProvidersWorkload')}</div>
</div>
<div className="text-2xl font-semibold text-zinc-100 truncate">{ekgTopProvider}</div>
<div className="mt-2 text-xs text-zinc-500">{t('dashboardWorkloadSnapshot')}</div>
</div>
<div className="rounded-2xl border border-zinc-800 bg-zinc-900/40 p-5 min-h-[148px]">
<div className="flex items-center gap-2 text-zinc-200 mb-2">
<Activity className="w-4 h-4 text-rose-400" />
<div className="text-sm font-medium">{t('taskAudit')}</div>
</div>
<div className="text-3xl font-semibold text-zinc-100">{recentFailures.length}</div>
<div className="mt-2 text-xs text-zinc-500">{t('dashboardRecentFailedTasks')}</div>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6 items-stretch">
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-6 min-h-[340px] h-full">
<div className="flex items-center gap-2 mb-5 text-zinc-200">
<Clock className="w-5 h-5 text-zinc-400" />
<h2 className="text-lg font-medium">{t('recentCron')}</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="rounded-xl border border-zinc-800 bg-zinc-950/60 p-4 min-h-[104px]">
<div className="text-[11px] uppercase tracking-widest text-zinc-500">{t('active')}</div>
<div className="mt-2 text-2xl font-semibold text-zinc-100">{enabledCron}</div>
</div>
<div className="rounded-xl border border-zinc-800 bg-zinc-950/60 p-4 min-h-[104px]">
<div className="text-[11px] uppercase tracking-widest text-zinc-500">{t('paused')}</div>
<div className="mt-2 text-2xl font-semibold text-zinc-100">{pausedCron}</div>
</div>
</div>
<div className="h-[180px] flex items-center justify-center text-sm text-zinc-500">
{cron.length === 0 ? t('noCronJobs') : `${cron.length} ${t('cronJobs')}`}
</div>
</div>
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-6 flex flex-col h-[400px]">
<h2 className="text-lg font-medium mb-4 flex items-center gap-2 text-zinc-200">
<Server className="w-5 h-5 text-zinc-400"/> {t('nodesSnapshot')}
</h2>
<div className="flex-1 bg-zinc-950/80 rounded-xl border border-zinc-800/50 p-4 overflow-auto">
{(() => {
try {
const parsedNodes = JSON.parse(nodes);
if (!Array.isArray(parsedNodes) || parsedNodes.length === 0) {
return <div className="text-sm text-zinc-500 text-center py-8">{t('noNodes')}</div>;
}
return (
<div className="space-y-3">
{parsedNodes.map((node: any, i: number) => (
<div key={node.id || i} className="flex items-center justify-between p-3 bg-zinc-900/50 rounded-xl border border-zinc-800/50">
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${node.online ? 'bg-emerald-500' : 'bg-red-500'}`} />
<span className="font-medium text-sm text-zinc-300">{node.name || node.id || `Node ${i + 1}`}</span>
</div>
<span className="text-xs font-mono text-zinc-500">{node.version || node.ip || ''}</span>
</div>
))}
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6 items-stretch">
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-6 min-h-[340px] h-full">
<div className="flex items-center gap-2 mb-5 text-zinc-200">
<Activity className="w-5 h-5 text-zinc-400" />
<h2 className="text-lg font-medium">{t('taskAudit')}</h2>
</div>
<div className="space-y-3">
{recentTasks.length === 0 ? (
<div className="text-sm text-zinc-500 text-center py-10">-</div>
) : recentTasks.map((task: any, index: number) => (
<div key={`${task.task_id || 'task'}-${index}`} className="rounded-xl border border-zinc-800 bg-zinc-950/60 p-4">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-zinc-200 truncate">{task.task_id || `task-${index + 1}`}</div>
<div className="text-xs text-zinc-500 truncate">{task.channel || '-'} · {task.source || '-'}</div>
</div>
);
} catch (e) {
return <pre className="font-mono text-[13px] leading-relaxed text-zinc-400">{nodes}</pre>;
}
})()}
<div className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${String(task.status || '').toLowerCase() === 'error' ? 'bg-rose-500/10 text-rose-300' : String(task.status || '').toLowerCase() === 'running' ? 'bg-emerald-500/10 text-emerald-300' : 'bg-zinc-800 text-zinc-400'}`}>
{task.status || '-'}
</div>
</div>
</div>
))}
</div>
</div>
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-6 min-h-[340px] h-full">
<div className="flex items-center gap-2 mb-5 text-zinc-200">
<AlertTriangle className="w-5 h-5 text-zinc-400" />
<h2 className="text-lg font-medium">{t('statusError')}</h2>
</div>
<div className="space-y-3">
{recentFailures.length === 0 ? (
<div className="text-sm text-zinc-500 text-center py-10">-</div>
) : recentFailures.map((task: any, index: number) => (
<div key={`${task.task_id || 'failed'}-${index}`} className="rounded-xl border border-zinc-800 bg-zinc-950/60 p-4">
<div className="text-sm font-medium text-zinc-200 truncate">{task.task_id || `task-${index + 1}`}</div>
<div className="text-xs text-zinc-500 truncate mt-1">{task.source || '-'} · {task.channel || '-'}</div>
<div className="text-xs text-rose-300 mt-2 break-all">{task.error || task.block_reason || '-'}</div>
</div>
))}
</div>
</div>
</div>