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