From be2381f128984c39ff1b99fc3b859b1e2a23a823 Mon Sep 17 00:00:00 2001 From: DBT Date: Sun, 1 Mar 2026 08:35:11 +0000 Subject: [PATCH] webui layout: add dedicated EKG menu/page and declutter task-audit layout --- webui/src/App.tsx | 2 + webui/src/components/Sidebar.tsx | 8 ++- webui/src/i18n/index.ts | 4 ++ webui/src/pages/EKG.tsx | 107 +++++++++++++++++++++++++++++ webui/src/pages/TaskAudit.tsx | 112 +------------------------------ 5 files changed, 121 insertions(+), 112 deletions(-) create mode 100644 webui/src/pages/EKG.tsx 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

+
+ + +
+
+ +
+
+
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
- -
-
-
-
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')}