diff --git a/src/App.tsx b/src/App.tsx index f489fed..b69bc05 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import ProjectDetailPage from '@/pages/ProjectDetailPage' import MilestonesPage from '@/pages/MilestonesPage' import MilestoneDetailPage from '@/pages/MilestoneDetailPage' import NotificationsPage from '@/pages/NotificationsPage' +import MonitorPage from '@/pages/MonitorPage' import axios from 'axios' const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080 @@ -61,7 +62,16 @@ export default function App() { if (!user) { return ( - +
+ +
+ + } /> + } /> + } /> + +
+
) } @@ -81,6 +91,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 749d8bf..e8d5eae 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ -import { useState, useEffect } from 'react' -import { Link, useLocation } from 'react-router-dom' -import api from '@/services/api' -import type { User } from '@/types' +import { useState, useEffect } from react +import { Link, useLocation } from react-router-dom +import api from @/services/api +import type { User } from @/types interface Props { user: User | null @@ -13,23 +13,30 @@ export default function Sidebar({ user, onLogout }: Props) { const [unreadCount, setUnreadCount] = useState(0) useEffect(() => { - api.get<{ count: number }>('/notifications/count') + if (!user) { + setUnreadCount(0) + return + } + api.get<{ count: number }>(/notifications/count) .then(({ data }) => setUnreadCount(data.count)) .catch(() => {}) const timer = setInterval(() => { - api.get<{ count: number }>('/notifications/count') + api.get<{ count: number }>(/notifications/count) .then(({ data }) => setUnreadCount(data.count)) .catch(() => {}) }, 30000) return () => clearInterval(timer) - }, []) + }, [user]) - const links = [ - { to: '/', icon: '📊', label: '仪表盘' }, - { to: '/issues', icon: '📋', label: 'Issues' }, - { to: '/projects', icon: '📁', label: '项目' }, - { to: '/milestones', icon: '🏁', label: '里程碑' }, - { to: '/notifications', icon: '🔔', label: `通知${unreadCount > 0 ? ` (${unreadCount})` : ''}` }, + const links = user ? [ + { to: /, icon: 📊, label: 仪表盘 }, + { to: /issues, icon: 📋, label: Issues }, + { to: /projects, icon: 📁, label: 项目 }, + { to: /milestones, icon: 🏁, label: 里程碑 }, + { to: /notifications, icon: 🔔, label: `通知${unreadCount > 0 ? ` (${unreadCount})` : }` }, + { to: /monitor, icon: 📡, label: Monitor }, + ] : [ + { to: /monitor, icon: 📡, label: Monitor }, ] return ( @@ -39,7 +46,7 @@ export default function Sidebar({ user, onLogout }: Props) {
    {links.map((l) => ( -
  • +
  • {l.icon} {l.label}
  • ))} diff --git a/src/pages/MonitorPage.tsx b/src/pages/MonitorPage.tsx new file mode 100644 index 0000000..fe58cf0 --- /dev/null +++ b/src/pages/MonitorPage.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react' +import api from '@/services/api' + +interface ProviderRow { + account_id: number + provider: string + label: string + usage_pct: number | null + status: string + error?: string | null + fetched_at?: string | null + reset_at?: string | null +} + +interface ServerRow { + server_id: number + identifier: string + display_name: string + online: boolean + openclaw_version?: string | null + cpu_pct?: number | null + mem_pct?: number | null + disk_pct?: number | null + swap_pct?: number | null + agents: Array<{ id?: string; name?: string; status?: string }> + last_seen_at?: string | null +} + +interface OverviewData { + issues: { + total_issues: number + new_issues_24h: number + processed_issues_24h: number + computed_at: string + } + providers: ProviderRow[] + servers: ServerRow[] + generated_at: string +} + +export default function MonitorPage() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + const load = async () => { + try { + const res = await api.get('/monitor/public/overview') + setData(res.data) + } finally { + setLoading(false) + } + } + + useEffect(() => { + load() + const t = setInterval(load, 30000) + return () => clearInterval(t) + }, []) + + if (loading) return
    Monitor loading...
    + if (!data) return
    Monitor load failed
    + + return ( +
    +

    📡 Monitor

    + +
    +

    Issue 概览(24小时窗口)

    +
      +
    • 所有项目总 Issue:{data.issues.total_issues}
    • +
    • 24小时新增:{data.issues.new_issues_24h}
    • +
    • 24小时已处理(resolved/closed):{data.issues.processed_issues_24h}
    • +
    +
    + +
    +

    Provider Usage

    + {data.providers.length === 0 ?

    暂无 provider 账号

    : ( +
      + {data.providers.map((p) => ( +
    • + {p.provider} / {p.label} · status: {p.status} + {p.usage_pct !== null ? : ''} + {p.reset_at ? : ''} + {p.error ? : ''} +
    • + ))} +
    + )} +
    + +
    +

    服务器监测

    + {data.servers.length === 0 ?

    暂无监测服务器

    : ( +
    + {data.servers.map((s) => ( +
    +
    + {s.display_name} ({s.identifier}) · {s.online ? '🟢 在线' : '🔴 离线'} +
    +
    + CPU: {s.cpu_pct ?? '-'}% · MEM: {s.mem_pct ?? '-'}% · DISK: {s.disk_pct ?? '-'}% · SWAP: {s.swap_pct ?? '-'}% +
    +
    OpenClaw: {s.openclaw_version || '-'}
    +
    Agents: {s.agents?.length || 0}
    +
    + ))} +
    + )} +
    +
    + ) +} diff --git a/src/services/api.ts b/src/services/api.ts index 0cd7b30..6ead39a 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -22,7 +22,9 @@ api.interceptors.response.use( (err) => { if (err.response?.status === 401) { localStorage.removeItem('token') - if (window.location.pathname !== '/login') { + const path = window.location.pathname + const publicPaths = ['/monitor', '/login'] + if (!publicPaths.some((p) => path.startsWith(p))) { window.location.href = '/login' } }