Add Fleet control plane and split manage.sh deploy menu.
Strategy nodes gain fleet token APIs; control/ app for local ops; manage.sh offers strategy vs control one-click deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -57,6 +57,9 @@ export default function SettingsPage() {
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [fleetConfigured, setFleetConfigured] = useState(false);
|
||||
const [fleetTokenInput, setFleetTokenInput] = useState("");
|
||||
const [fleetHint, setFleetHint] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [ok, setOk] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -526,7 +529,15 @@ export default function SettingsPage() {
|
||||
<button
|
||||
type="button"
|
||||
className={tab === "account" ? "tab active" : "tab"}
|
||||
onClick={() => setTab("account")}
|
||||
onClick={() => {
|
||||
setTab("account");
|
||||
apiFetch<{ configured: boolean; hint?: string }>("/api/fleet/meta")
|
||||
.then((r) => {
|
||||
setFleetConfigured(!!r.configured);
|
||||
setFleetHint(r.hint || "");
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}}
|
||||
>
|
||||
登录账户
|
||||
</button>
|
||||
@@ -1648,7 +1659,83 @@ export default function SettingsPage() {
|
||||
<div className="card settings-card">
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
{ok ? <div className="settings-ok">{ok}</div> : null}
|
||||
<section className="settings-section">
|
||||
<h3>中控 API Token</h3>
|
||||
<p className="meta">
|
||||
{fleetHint ||
|
||||
"在中控生成 Token 后粘贴保存。用于远程启停、更新与免密登录。"}
|
||||
</p>
|
||||
<p className="meta">
|
||||
状态:{fleetConfigured ? "已配置" : "未配置"}
|
||||
</p>
|
||||
<div className="settings-fields">
|
||||
<div className="field">
|
||||
<label htmlFor="fleetTok">Token</label>
|
||||
<input
|
||||
id="fleetTok"
|
||||
type="password"
|
||||
value={fleetTokenInput}
|
||||
onChange={(e) => setFleetTokenInput(e.target.value)}
|
||||
placeholder="粘贴中控生成的 Token"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-actions">
|
||||
<button
|
||||
className="btn"
|
||||
type="button"
|
||||
disabled={loading || !fleetTokenInput.trim()}
|
||||
onClick={async () => {
|
||||
setErr("");
|
||||
setOk("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const r = await apiFetch<{ configured: boolean }>(
|
||||
"/api/fleet/token",
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ token: fleetTokenInput.trim() }),
|
||||
},
|
||||
);
|
||||
setFleetConfigured(!!r.configured);
|
||||
setFleetTokenInput("");
|
||||
setOk("中控 Token 已保存");
|
||||
} catch (ex) {
|
||||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存 Token
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={loading || !fleetConfigured}
|
||||
onClick={async () => {
|
||||
setErr("");
|
||||
setOk("");
|
||||
setLoading(true);
|
||||
try {
|
||||
await apiFetch("/api/fleet/token", { method: "DELETE" });
|
||||
setFleetConfigured(false);
|
||||
setOk("已清除中控 Token");
|
||||
} catch (ex) {
|
||||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<form onSubmit={onSaveCreds} className="settings-account-form">
|
||||
<section className="settings-section">
|
||||
<h3>登录账号</h3>
|
||||
<div className="settings-fields">
|
||||
<div className="field">
|
||||
<label htmlFor="user">新用户名</label>
|
||||
@@ -1695,6 +1782,7 @@ export default function SettingsPage() {
|
||||
{loading ? "保存中…" : "保存账号"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user