From bc1714281c72db31145707e0e3feb1b90b31b93d Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Mar 2026 11:59:56 +0000 Subject: [PATCH 1/8] feat: allow public monitor route without login and add monitor page --- src/App.tsx | 13 ++++- src/components/Sidebar.tsx | 35 +++++++----- src/pages/MonitorPage.tsx | 113 +++++++++++++++++++++++++++++++++++++ src/services/api.ts | 4 +- 4 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 src/pages/MonitorPage.tsx 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' } } From 18b1a51f3f9b20e050c0c82854942d5db552d390 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Mar 2026 12:41:35 +0000 Subject: [PATCH 2/8] feat: add monitor admin panel for provider/server management and challenge generation --- src/pages/MonitorPage.tsx | 140 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 4 deletions(-) diff --git a/src/pages/MonitorPage.tsx b/src/pages/MonitorPage.tsx index fe58cf0..8e7599e 100644 --- a/src/pages/MonitorPage.tsx +++ b/src/pages/MonitorPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import api from '@/services/api' interface ProviderRow { @@ -38,25 +38,116 @@ interface OverviewData { generated_at: string } +interface AdminUser { + id: number + is_admin: boolean +} + +interface ProviderAccountItem { + id: number + provider: string + label: string + is_enabled: boolean + credential_masked: string +} + +interface ServerItem { + server_id: number + identifier: string + display_name: string + online: boolean +} + export default function MonitorPage() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) + const [isAdmin, setIsAdmin] = useState(false) + const [providerAccounts, setProviderAccounts] = useState([]) + const [servers, setServers] = useState([]) + + const [providerForm, setProviderForm] = useState({ provider: 'openai', label: '', credential: '' }) + const [providerTestMsg, setProviderTestMsg] = useState('') + const [serverForm, setServerForm] = useState({ identifier: '', display_name: '' }) + + const canAdmin = useMemo(() => !!localStorage.getItem('token') && isAdmin, [isAdmin]) const load = async () => { try { const res = await api.get('/monitor/public/overview') setData(res.data) + const token = localStorage.getItem('token') + if (token) { + try { + const me = await api.get('/auth/me') + setIsAdmin(!!me.data.is_admin) + } catch { + setIsAdmin(false) + } + } else { + setIsAdmin(false) + } } finally { setLoading(false) } } + const loadAdminData = async () => { + if (!canAdmin) return + const [p, s] = await Promise.all([ + api.get('/monitor/admin/providers/accounts'), + api.get('/monitor/admin/servers'), + ]) + setProviderAccounts(p.data) + setServers(s.data) + } + useEffect(() => { load() const t = setInterval(load, 30000) return () => clearInterval(t) }, []) + useEffect(() => { + loadAdminData() + }, [canAdmin]) + + const testProvider = async () => { + const r = await api.post<{ ok: boolean; message: string }>('/monitor/admin/providers/test', { + provider: providerForm.provider, + credential: providerForm.credential, + }) + setProviderTestMsg((r.data.ok ? '✅ ' : '❌ ') + r.data.message) + } + + const addProvider = async () => { + await api.post('/monitor/admin/providers/accounts', providerForm) + setProviderForm({ ...providerForm, label: '', credential: '' }) + await loadAdminData() + } + + const deleteProvider = async (id: number) => { + await api.delete('/monitor/admin/providers/accounts/' + id) + await loadAdminData() + } + + const addServer = async () => { + await api.post('/monitor/admin/servers', serverForm) + setServerForm({ identifier: '', display_name: '' }) + await loadAdminData() + } + + const deleteServer = async (id: number) => { + await api.delete('/monitor/admin/servers/' + id) + await loadAdminData() + } + + const createChallenge = async (id: number) => { + const r = await api.post<{ identifier: string; challenge_uuid: string; expires_at: string }>('/monitor/admin/servers/' + id + '/challenge') + alert('identifier=' + r.data.identifier + ' +challenge_uuid=' + r.data.challenge_uuid + ' +expires_at=' + r.data.expires_at) + } + if (loading) return
    Monitor loading...
    if (!data) return
    Monitor load failed
    @@ -80,9 +171,9 @@ export default function MonitorPage() { {data.providers.map((p) => (
  • {p.provider} / {p.label} · status: {p.status} - {p.usage_pct !== null ? : ''} - {p.reset_at ? : ''} - {p.error ? : ''} + {p.usage_pct !== null ? (' · usage: ' + p.usage_pct + '%') : ''} + {p.reset_at ? (' · reset: ' + p.reset_at) : ''} + {p.error ? (' · error: ' + p.error) : ''}
  • ))}
@@ -108,6 +199,47 @@ export default function MonitorPage() { )} + + {canAdmin && ( +
+

Admin: Provider 管理

+
+ + setProviderForm({ ...providerForm, label: e.target.value })} /> + setProviderForm({ ...providerForm, credential: e.target.value })} /> + + +
+ {providerTestMsg &&

{providerTestMsg}

} +
    + {providerAccounts.map((p) => ( +
  • {p.provider} / {p.label} / {p.credential_masked}
  • + ))} +
+ +

Admin: 服务器管理

+
+ setServerForm({ ...serverForm, identifier: e.target.value })} /> + setServerForm({ ...serverForm, display_name: e.target.value })} /> + +
+
    + {servers.map((s) => ( +
  • + {s.display_name} ({s.identifier}) + + +
  • + ))} +
+
+ )} ) } From d574258c8e44dcfc6aa50a438ae505c24ab09ddb Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Mar 2026 13:15:20 +0000 Subject: [PATCH 3/8] feat: show provider window and last update in monitor view --- src/pages/MonitorPage.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/MonitorPage.tsx b/src/pages/MonitorPage.tsx index 8e7599e..b1a40cb 100644 --- a/src/pages/MonitorPage.tsx +++ b/src/pages/MonitorPage.tsx @@ -10,6 +10,7 @@ interface ProviderRow { error?: string | null fetched_at?: string | null reset_at?: string | null + window?: string | null } interface ServerRow { @@ -173,6 +174,8 @@ expires_at=' + r.data.expires_at) {p.provider} / {p.label} · status: {p.status} {p.usage_pct !== null ? (' · usage: ' + p.usage_pct + '%') : ''} {p.reset_at ? (' · reset: ' + p.reset_at) : ''} + {p.window ? (' · window: ' + p.window) : ''} + {p.fetched_at ? (' · updated: ' + p.fetched_at) : ''} {p.error ? (' · error: ' + p.error) : ''} ))} From ddcbd289673498e7d8104ec9367a12bcf64dbac1 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Mar 2026 17:26:37 +0000 Subject: [PATCH 4/8] fix: repair monitor sidebar and alert rendering --- src/components/Sidebar.tsx | 28 ++++++++++++++-------------- src/pages/MonitorPage.tsx | 4 +--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e8d5eae..5237e4f 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 @@ -17,11 +17,11 @@ export default function Sidebar({ user, onLogout }: Props) { setUnreadCount(0) return } - api.get<{ count: number }>(/notifications/count) + 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) @@ -29,14 +29,14 @@ export default function Sidebar({ user, onLogout }: Props) { }, [user]) 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: '/', 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 }, + { to: '/monitor', icon: '📡', label: 'Monitor' }, ] return ( @@ -46,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 index b1a40cb..b739ab2 100644 --- a/src/pages/MonitorPage.tsx +++ b/src/pages/MonitorPage.tsx @@ -144,9 +144,7 @@ export default function MonitorPage() { const createChallenge = async (id: number) => { const r = await api.post<{ identifier: string; challenge_uuid: string; expires_at: string }>('/monitor/admin/servers/' + id + '/challenge') - alert('identifier=' + r.data.identifier + ' -challenge_uuid=' + r.data.challenge_uuid + ' -expires_at=' + r.data.expires_at) + alert('identifier=' + r.data.identifier + '\nchallenge_uuid=' + r.data.challenge_uuid + '\nexpires_at=' + r.data.expires_at) } if (loading) return
    Monitor loading...
    From 34ab80e50d135eed351ce38ea71836ba751339f4 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Mar 2026 17:49:44 +0000 Subject: [PATCH 5/8] style: align monitor page with dashboard look and add monitor styles --- src/index.css | 18 ++++ src/pages/MonitorPage.tsx | 189 +++++++++++++++++++++++--------------- 2 files changed, 132 insertions(+), 75 deletions(-) diff --git a/src/index.css b/src/index.css index adc51c4..1b351bf 100644 --- a/src/index.css +++ b/src/index.css @@ -183,3 +183,21 @@ dd { font-size: .9rem; } .setup-done { text-align: center; } .setup-done h2 { color: var(--success); margin-bottom: 12px; } @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } + + +/* Monitor */ +.monitor-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; margin-top: 12px; } +.monitor-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; } +.monitor-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; } +.monitor-metrics { margin: 8px 0; font-size: .9rem; } +.monitor-admin { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; } + +.status-ok { background: var(--success); } +.status-error { background: var(--danger); } +.status-pending { background: var(--warning); } +.status-online { background: var(--success); } +.status-offline { background: var(--danger); } + +.btn-secondary { background: none; border: 1px solid var(--border); color: var(--text); padding: 6px 12px; border-radius: 6px; cursor: pointer; } +.btn-danger { background: var(--danger); color: #fff; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; } +.btn-danger:hover { opacity: .9; } diff --git a/src/pages/MonitorPage.tsx b/src/pages/MonitorPage.tsx index b739ab2..8c00785 100644 --- a/src/pages/MonitorPage.tsx +++ b/src/pages/MonitorPage.tsx @@ -11,6 +11,8 @@ interface ProviderRow { fetched_at?: string | null reset_at?: string | null window?: string | null + used?: number | null + limit?: number | null } interface ServerRow { @@ -144,102 +146,139 @@ export default function MonitorPage() { const createChallenge = async (id: number) => { const r = await api.post<{ identifier: string; challenge_uuid: string; expires_at: string }>('/monitor/admin/servers/' + id + '/challenge') - alert('identifier=' + r.data.identifier + '\nchallenge_uuid=' + r.data.challenge_uuid + '\nexpires_at=' + r.data.expires_at) + alert('identifier=' + r.data.identifier + ' | challenge_uuid=' + r.data.challenge_uuid + ' | expires_at=' + r.data.expires_at) } - if (loading) return
    Monitor loading...
    - if (!data) return
    Monitor load failed
    + 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}
    • -
    -
    +
    +
    + {data.issues.total_issues} + 总 Issues +
    +
    + {data.issues.new_issues_24h} + 24小时新增 +
    +
    + {data.issues.processed_issues_24h} + 24小时已处理 +
    +
    -
    -

    Provider Usage

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

    暂无 provider 账号

    : ( -
      - {data.providers.map((p) => ( -
    • - {p.provider} / {p.label} · status: {p.status} - {p.usage_pct !== null ? (' · usage: ' + p.usage_pct + '%') : ''} - {p.reset_at ? (' · reset: ' + p.reset_at) : ''} - {p.window ? (' · window: ' + p.window) : ''} - {p.fetched_at ? (' · updated: ' + p.fetched_at) : ''} - {p.error ? (' · error: ' + p.error) : ''} -
    • - ))} -
    +
    +
    +

    Provider Usage

    + 更新于 {data.generated_at} +
    + {data.providers.length === 0 ?

    暂无 provider 账号

    : ( + + + + + + + + + + + + + + {data.providers.map((p) => ( + + + + + + + + + + ))} + +
    ProviderLabelUsageWindowResetStatusUpdated
    {p.provider}{p.label}{p.usage_pct !== null ? p.usage_pct + '%' : '-'}{p.window || '-'}{p.reset_at || '-'}{p.status}{p.fetched_at || '-'}
    )} -
    +
    -
    +

    服务器监测

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

    暂无监测服务器

    : ( -
    + {data.servers.length === 0 ?

    暂无监测服务器

    : ( +
    {data.servers.map((s) => ( -
    -
    - {s.display_name} ({s.identifier}) · {s.online ? '🟢 在线' : '🔴 离线'} +
    +
    +
    + {s.display_name} +
    {s.identifier}
    +
    + {s.online ? 'online' : 'offline'}
    -
    - CPU: {s.cpu_pct ?? '-'}% · MEM: {s.mem_pct ?? '-'}% · DISK: {s.disk_pct ?? '-'}% · SWAP: {s.swap_pct ?? '-'}% +
    + 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}
    +
    OpenClaw: {s.openclaw_version || '-'}
    +
    Agents: {s.agents?.length || 0}
    ))}
    )} -
    +
    {canAdmin && ( -
    -

    Admin: Provider 管理

    -
    - - setProviderForm({ ...providerForm, label: e.target.value })} /> - setProviderForm({ ...providerForm, credential: e.target.value })} /> - - -
    - {providerTestMsg &&

    {providerTestMsg}

    } -
      - {providerAccounts.map((p) => ( -
    • {p.provider} / {p.label} / {p.credential_masked}
    • - ))} -
    +
    +

    Admin 管理

    -

    Admin: 服务器管理

    -
    - setServerForm({ ...serverForm, identifier: e.target.value })} /> - setServerForm({ ...serverForm, display_name: e.target.value })} /> - +
    +
    +

    Provider 账号

    +
    + + setProviderForm({ ...providerForm, label: e.target.value })} /> + setProviderForm({ ...providerForm, credential: e.target.value })} /> + + +
    + {providerTestMsg &&

    {providerTestMsg}

    } +
      + {providerAccounts.map((p) => ( +
    • {p.provider} / {p.label} / {p.credential_masked}
    • + ))} +
    +
    + +
    +

    服务器

    +
    + setServerForm({ ...serverForm, identifier: e.target.value })} /> + setServerForm({ ...serverForm, display_name: e.target.value })} /> + +
    +
      + {servers.map((s) => ( +
    • + {s.display_name} ({s.identifier}) + + +
    • + ))} +
    +
    -
      - {servers.map((s) => ( -
    • - {s.display_name} ({s.identifier}) - - -
    • - ))} -
    -
    + )} ) From 0ab1d2f380a2f9c2ef0d9164602d6fe716d3f485 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Mar 2026 21:19:54 +0000 Subject: [PATCH 6/8] i18n: translate frontend UI strings to English --- src/App.tsx | 4 +- src/components/Sidebar.tsx | 10 ++-- src/pages/CreateIssuePage.tsx | 16 +++--- src/pages/DashboardPage.tsx | 12 ++--- src/pages/IssueDetailPage.tsx | 26 +++++----- src/pages/IssuesPage.tsx | 10 ++-- src/pages/LoginPage.tsx | 10 ++-- src/pages/MilestoneDetailPage.tsx | 16 +++--- src/pages/MilestonesPage.tsx | 20 ++++---- src/pages/MonitorPage.tsx | 32 ++++++------ src/pages/NotificationsPage.tsx | 8 +-- src/pages/ProjectDetailPage.tsx | 32 ++++++------ src/pages/ProjectsPage.tsx | 16 +++--- src/pages/SetupWizardPage.tsx | 82 +++++++++++++++---------------- 14 files changed, 147 insertions(+), 147 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b69bc05..94ef3f5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,14 +50,14 @@ export default function App() { } if (appState === 'checking') { - return
    检查配置状态...
    + return
    Checking configuration status...
    } if (appState === 'setup') { return } - if (loading) return
    加载中...
    + if (loading) return
    Loading...
    if (!user) { return ( diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 5237e4f..8906839 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -29,11 +29,11 @@ export default function Sidebar({ user, onLogout }: Props) { }, [user]) const links = user ? [ - { to: '/', icon: '📊', label: '仪表盘' }, + { to: '/', icon: '📊', label: 'Dashboard' }, { to: '/issues', icon: '📋', label: 'Issues' }, - { to: '/projects', icon: '📁', label: '项目' }, - { to: '/milestones', icon: '🏁', label: '里程碑' }, - { to: '/notifications', icon: '🔔', label: '通知' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') }, + { to: '/projects', icon: '📁', label: 'Projects' }, + { to: '/milestones', icon: '🏁', label: 'Milestones' }, + { to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') }, { to: '/monitor', icon: '📡', label: 'Monitor' }, ] : [ { to: '/monitor', icon: '📡', label: 'Monitor' }, @@ -54,7 +54,7 @@ export default function Sidebar({ user, onLogout }: Props) { {user && (
    👤 {user.username} - +
    )} diff --git a/src/pages/CreateIssuePage.tsx b/src/pages/CreateIssuePage.tsx index 86a175d..4f0cc0f 100644 --- a/src/pages/CreateIssuePage.tsx +++ b/src/pages/CreateIssuePage.tsx @@ -27,16 +27,16 @@ export default function CreateIssuePage() { return (
    -

    新建 Issue

    +

    Create Issue

    - -