diff --git a/webui/src/App.tsx b/webui/src/App.tsx index a52f129..ca78e72 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -12,6 +12,7 @@ import Logs from './pages/Logs'; import Skills from './pages/Skills'; import Memory from './pages/Memory'; import TaskAudit from './pages/TaskAudit'; +import EKG from './pages/EKG'; import Tasks from './pages/Tasks'; export default function App() { @@ -30,6 +31,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/webui/src/components/Sidebar.tsx b/webui/src/components/Sidebar.tsx index 8999300..ef86dfb 100644 --- a/webui/src/components/Sidebar.tsx +++ b/webui/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { LayoutDashboard, MessageSquare, Settings, Clock, Server, Terminal, Zap, FolderOpen, ClipboardList, ListTodo } from 'lucide-react'; +import { LayoutDashboard, MessageSquare, Settings, Clock, Server, Terminal, Zap, FolderOpen, ClipboardList, ListTodo, BrainCircuit } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import NavItem from './NavItem'; @@ -34,6 +34,12 @@ const Sidebar: React.FC = () => { { icon: , label: t('tasks'), to: '/tasks' }, ], }, + { + title: t('sidebarInsights'), + items: [ + { icon: , label: t('ekg'), to: '/ekg' }, + ], + }, ]; return ( diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts index 1be9386..d0b40d8 100644 --- a/webui/src/i18n/index.ts +++ b/webui/src/i18n/index.ts @@ -18,6 +18,8 @@ const resources = { sidebarCore: 'Core', sidebarSystem: 'System', sidebarOps: 'Operations', + sidebarInsights: 'Insights', + ekg: 'EKG', taskList: 'Task List', taskDetail: 'Task Detail', taskQueue: 'Task Queue', @@ -191,6 +193,8 @@ const resources = { sidebarCore: '核心', sidebarSystem: '系统', sidebarOps: '运维', + sidebarInsights: '洞察', + ekg: 'EKG', taskList: '任务列表', taskDetail: '任务详情', taskQueue: '任务队列', diff --git a/webui/src/pages/EKG.tsx b/webui/src/pages/EKG.tsx new file mode 100644 index 0000000..3340e80 --- /dev/null +++ b/webui/src/pages/EKG.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react'; +import { useAppContext } from '../context/AppContext'; + +type EKGKV = { key?: string; score?: number; count?: number }; + +const EKG: React.FC = () => { + const { q } = useAppContext(); + const [loading, setLoading] = useState(false); + const [ekgWindow, setEkgWindow] = useState<'6h' | '24h' | '7d'>(() => { + const saved = typeof window !== 'undefined' ? window.localStorage.getItem('taskAudit.ekgWindow') : null; + return saved === '6h' || saved === '24h' || saved === '7d' ? saved : '24h'; + }); + const [providerTop, setProviderTop] = useState([]); + const [providerTopWorkload, setProviderTopWorkload] = useState([]); + const [errsigTop, setErrsigTop] = useState([]); + const [errsigTopHeartbeat, setErrsigTopHeartbeat] = useState([]); + const [errsigTopWorkload, setErrsigTopWorkload] = useState([]); + const [sourceStats, setSourceStats] = useState>({}); + const [channelStats, setChannelStats] = useState>({}); + const [escalationCount, setEscalationCount] = useState(0); + + const fetchData = async () => { + setLoading(true); + try { + const ekgJoin = q ? `${q}&window=${encodeURIComponent(ekgWindow)}` : `?window=${encodeURIComponent(ekgWindow)}`; + const er = await fetch(`/webui/api/ekg_stats${ekgJoin}`); + if (!er.ok) throw new Error(await er.text()); + const ej = await er.json(); + setProviderTop(Array.isArray(ej.provider_top) ? ej.provider_top : []); + setProviderTopWorkload(Array.isArray(ej.provider_top_workload) ? ej.provider_top_workload : []); + setErrsigTop(Array.isArray(ej.errsig_top) ? ej.errsig_top : []); + setErrsigTopHeartbeat(Array.isArray(ej.errsig_top_heartbeat) ? ej.errsig_top_heartbeat : []); + setErrsigTopWorkload(Array.isArray(ej.errsig_top_workload) ? ej.errsig_top_workload : []); + setSourceStats(ej.source_stats && typeof ej.source_stats === 'object' ? ej.source_stats : {}); + setChannelStats(ej.channel_stats && typeof ej.channel_stats === 'object' ? ej.channel_stats : {}); + setEscalationCount(Number(ej.escalation_count || 0)); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchData(); }, [q, ekgWindow]); + useEffect(() => { + if (typeof window !== 'undefined') window.localStorage.setItem('taskAudit.ekgWindow', ekgWindow); + }, [ekgWindow]); + + return ( + + + EKG + + setEkgWindow(e.target.value as '6h' | '24h' | '7d')} className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs"> + 6h + 24h + 7d + + {loading ? 'Loading...' : 'Refresh'} + + + + + + Escalations + {escalationCount} + + + Source Stats + {Object.keys(sourceStats).length === 0 ? - : Object.entries(sourceStats).map(([k,v]) => {k}: {v})} + + + Channel Stats + {Object.keys(channelStats).length === 0 ? - : Object.entries(channelStats).map(([k,v]) => {k}: {v})} + + + + + + Top Providers (workload) + {providerTopWorkload.length === 0 ? - : providerTopWorkload.map((x,i)=>{x.key} ({Number(x.score||0).toFixed(2)}))} + + + Top Providers (all) + {providerTop.length === 0 ? - : providerTop.map((x,i)=>{x.key} ({Number(x.score||0).toFixed(2)}))} + + + + + + Top Error Signatures (workload) + {errsigTopWorkload.length === 0 ? - : errsigTopWorkload.map((x,i)=>{x.key} (x{x.count||0}))} + + + Top Error Signatures (heartbeat) + {errsigTopHeartbeat.length === 0 ? - : errsigTopHeartbeat.map((x,i)=>{x.key} (x{x.count||0}))} + + + Top Error Signatures (all) + {errsigTop.length === 0 ? - : errsigTop.map((x,i)=>{x.key} (x{x.count||0}))} + + + + ); +}; + +export default EKG; diff --git a/webui/src/pages/TaskAudit.tsx b/webui/src/pages/TaskAudit.tsx index 65cc556..435e465 100644 --- a/webui/src/pages/TaskAudit.tsx +++ b/webui/src/pages/TaskAudit.tsx @@ -2,8 +2,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; -type EKGKV = { key?: string; score?: number; count?: number }; - type TaskAuditItem = { task_id?: string; time?: string; @@ -39,18 +37,6 @@ const TaskAudit: React.FC = () => { const [dailyReport, setDailyReport] = useState(''); const [reportDate, setReportDate] = useState(new Date().toISOString().slice(0,10)); const [showDailyReport, setShowDailyReport] = useState(false); - const [ekgProviderTop, setEkgProviderTop] = useState([]); - const [ekgProviderTopWorkload, setEkgProviderTopWorkload] = useState([]); - const [ekgErrsigTop, setEkgErrsigTop] = useState([]); - const [ekgErrsigTopHeartbeat, setEkgErrsigTopHeartbeat] = useState([]); - const [ekgErrsigTopWorkload, setEkgErrsigTopWorkload] = useState([]); - const [ekgSourceStats, setEkgSourceStats] = useState>({}); - const [ekgChannelStats, setEkgChannelStats] = useState>({}); - const [ekgEscalationCount, setEkgEscalationCount] = useState(0); - const [ekgWindow, setEkgWindow] = useState<'6h' | '24h' | '7d'>(() => { - const saved = typeof window !== 'undefined' ? window.localStorage.getItem('taskAudit.ekgWindow') : null; - return saved === '6h' || saved === '24h' || saved === '7d' ? saved : '24h'; - }); const fetchData = async () => { setLoading(true); @@ -71,20 +57,6 @@ const TaskAudit: React.FC = () => { } else { setDailyReport(''); } - const ekgJoin = q ? `${q}&window=${encodeURIComponent(ekgWindow)}` : `?window=${encodeURIComponent(ekgWindow)}`; - const ekgUrl = `/webui/api/ekg_stats${ekgJoin}`; - const er = await fetch(ekgUrl); - if (er.ok) { - const ej = await er.json(); - setEkgProviderTop(Array.isArray(ej.provider_top) ? ej.provider_top : []); - setEkgProviderTopWorkload(Array.isArray(ej.provider_top_workload) ? ej.provider_top_workload : []); - setEkgErrsigTop(Array.isArray(ej.errsig_top) ? ej.errsig_top : []); - setEkgErrsigTopHeartbeat(Array.isArray(ej.errsig_top_heartbeat) ? ej.errsig_top_heartbeat : []); - setEkgErrsigTopWorkload(Array.isArray(ej.errsig_top_workload) ? ej.errsig_top_workload : []); - setEkgSourceStats(ej.source_stats && typeof ej.source_stats === 'object' ? ej.source_stats : {}); - setEkgChannelStats(ej.channel_stats && typeof ej.channel_stats === 'object' ? ej.channel_stats : {}); - setEkgEscalationCount(Number(ej.escalation_count || 0)); - } } catch (e) { console.error(e); setItems([]); @@ -94,13 +66,8 @@ const TaskAudit: React.FC = () => { } }; - useEffect(() => { fetchData(); }, [q, reportDate, ekgWindow]); + useEffect(() => { fetchData(); }, [q, reportDate]); - useEffect(() => { - if (typeof window !== 'undefined') { - window.localStorage.setItem('taskAudit.ekgWindow', ekgWindow); - } - }, [ekgWindow]); @@ -159,83 +126,6 @@ const TaskAudit: React.FC = () => { - - - EKG Insights - setEkgWindow(e.target.value as '6h' | '24h' | '7d')} className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs"> - 6h - 24h - 7d - - - - - Escalations - {ekgEscalationCount} - - - Source Stats - - {Object.keys(ekgSourceStats).length === 0 ? - : Object.entries(ekgSourceStats).map(([k, v]) => ( - {k}: {v} - ))} - - - - Channel Stats - - {Object.keys(ekgChannelStats).length === 0 ? - : Object.entries(ekgChannelStats).map(([k, v]) => ( - {k}: {v} - ))} - - - - - - Top Providers (workload) - - {ekgProviderTopWorkload.length === 0 ? - : ekgProviderTopWorkload.map((x, i) => ( - {x.key} ({Number(x.score || 0).toFixed(2)}) - ))} - - - - Top Providers (all) - - {ekgProviderTop.length === 0 ? - : ekgProviderTop.map((x, i) => ( - {x.key} ({Number(x.score || 0).toFixed(2)}) - ))} - - - - - - Top Error Signatures (workload) - - {ekgErrsigTopWorkload.length === 0 ? - : ekgErrsigTopWorkload.map((x, i) => ( - {x.key} (x{x.count || 0}) - ))} - - - - Top Error Signatures (heartbeat) - - {ekgErrsigTopHeartbeat.length === 0 ? - : ekgErrsigTopHeartbeat.map((x, i) => ( - {x.key} (x{x.count || 0}) - ))} - - - - Top Error Signatures (all) - - {ekgErrsigTop.length === 0 ? - : ekgErrsigTop.map((x, i) => ( - {x.key} (x{x.count || 0}) - ))} - - - - - {t('dailySummary')}