diff --git a/web/README.md b/web/README.md
index a111d32..298fcfa 100644
--- a/web/README.md
+++ b/web/README.md
@@ -19,7 +19,7 @@ npm run gen:api # 后端接口变了之后,重新生成 shared/
npm run lint:css # 颜色只能写在 shared/theme/tokens.css,其他样式一律引用令牌
```
-连别的后端(本机跑前端、连线上的):点页面右上角(管理端登录页右下角)的插头图标,填后端地址和 API 令牌(展示端:站点私有时需要只读令牌;管理端:admin 范围的令牌),保存后刷新。设置存在浏览器里;跨域时不带 Cookie,所以后端要设 `Monitor__CorsOrigins=<这个前端的来源>`(如 `http://localhost:5173`)。也可以用构建时的 `VITE_API_BASE` 给默认地址。
+连别的后端(本机跑前端、连线上的):登录页左边选后端(这个页面自己所在的、保存过的,或者「添加后端」),右边都是用户名和密码;添加后端填地址、用户名、密码,登录成功才保存。跨域用不了 Cookie,所以登录时后端会返回会话令牌(`return_session`),前端存在浏览器里、之后每个请求带上,12 小时过期、退出登录即作废;用户看不到也不需要管它。后端需要放行这个页面的来源(`Monitor__CorsOrigins`)。展示端和管理端侧栏的插头图标是同一个切换器。
约定:
- 展示端能拿到的主机数据只有汇总的数字(CPU、内存、磁盘占用,网卡累计流量与瞬时网速,运行时长),**没有 CPU 型号、主机名、网卡名,也没有任何 IP**——后端的公开接口不返回它们,Agent 也根本不采集。展示端不显示 Probe 的版本和来源地址(这些只在管理端)。
diff --git a/web/admin/src/App.tsx b/web/admin/src/App.tsx
index a3244f2..c930c93 100644
--- a/web/admin/src/App.tsx
+++ b/web/admin/src/App.tsx
@@ -1,7 +1,7 @@
-import { FlaskConical, ListChecks, LogOut, Radar, Server, Settings } from 'lucide-react'
+import { FlaskConical, ListChecks, LogOut, Plus, Radar, Server, Settings } from 'lucide-react'
import { useState, type FormEvent } from 'react'
import { NavLink, Navigate, Route, Routes } from 'react-router-dom'
-import { isRemote } from '@shared/api/source'
+import { activateSource, getBase, listSources } from '@shared/api/source'
import { describeError, Field, SourceButton, Spinner, ThemeToggle } from '@shared/ui'
import { useAuth } from './auth'
import { AgentDetail } from './pages/AgentDetail'
@@ -11,22 +11,34 @@ import { System } from './pages/System'
import { Tasks } from './pages/Tasks'
import { Tests } from './pages/Tests'
+/** 登录页:左边选后端(这个页面自己所在的,或保存过的,或添加一个),右边都是用户名 + 密码。 */
function Login() {
- const { login, loginWithToken } = useAuth()
- const remote = isRemote()
- const [token, setTokenText] = useState('')
+ const { login } = useAuth()
+ const active = getBase()
+ const saved = listSources()
+ const [adding, setAdding] = useState(false)
+ const [url, setUrl] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
+ const choose = (base: string) => {
+ setAdding(false)
+ setError('')
+ setPassword('')
+ if (base !== active) {
+ activateSource(base)
+ location.reload()
+ }
+ }
+
const submit = async (e: FormEvent) => {
e.preventDefault()
setBusy(true)
setError('')
try {
- if (remote) await loginWithToken(token)
- else await login(username, password)
+ await login(username, password, adding ? url : undefined)
} catch (err) {
setError(describeError(err))
} finally {
@@ -34,35 +46,67 @@ function Login() {
}
}
+ const rows = [{ base: '', label: location.host }, ...saved.map((x) => ({ base: x.base, label: x.base.replace(/^https?:\/\//i, '') }))]
+ const title = adding ? '添加后端' : active ? active.replace(/^https?:\/\//i, '') : location.origin
+
return (
)
}
@@ -102,6 +146,7 @@ export function App() {
))}
+
{(me.username ?? 'A').slice(0, 1).toUpperCase()}
@@ -110,7 +155,6 @@ export function App() {
{me.scope}
-