mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: add buttons to update hermes and restart gateway
This commit is contained in:
parent
ea06104a3c
commit
fc21c14206
9 changed files with 492 additions and 70 deletions
26
web/package-lock.json
generated
26
web/package-lock.json
generated
|
|
@ -70,6 +70,7 @@
|
|||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
|
|
@ -1103,6 +1104,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.17.tgz",
|
||||
"integrity": "sha512-/qaXP/7mc4MUS0s4cPPFASDRjtsWp85/TbfsciqDgU1HwYixbSbbytNuInD8AcTYC3xaxACgVX06agdfQy9W+g==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"interval-tree-1d": "^1.0.0",
|
||||
|
|
@ -1755,6 +1757,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.0.tgz",
|
||||
"integrity": "sha512-90abYK2q5/qDM+GACs9zRvc5KhEEpEWqWlHSd64zTPNxg+9wCJvTfyD9x2so7hlQhjRYO1Fa6flR3BC/kpTFkA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/webxr": "*",
|
||||
|
|
@ -2489,6 +2492,7 @@
|
|||
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
|
|
@ -2498,6 +2502,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -2508,6 +2513,7 @@
|
|||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
|
|
@ -2572,6 +2578,7 @@
|
|||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
|
|
@ -2867,6 +2874,7 @@
|
|||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -3019,6 +3027,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -3526,6 +3535,7 @@
|
|||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -3839,6 +3849,7 @@
|
|||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -4217,7 +4228,8 @@
|
|||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
|
||||
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
|
||||
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
||||
"license": "Standard 'no charge' license: https://gsap.com/standard-license.",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
|
|
@ -4532,6 +4544,7 @@
|
|||
"resolved": "https://registry.npmjs.org/leva/-/leva-0.10.1.tgz",
|
||||
"integrity": "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@radix-ui/react-portal": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
|
|
@ -4953,6 +4966,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.0.0 || >=22.0.0"
|
||||
}
|
||||
|
|
@ -5080,6 +5094,7 @@
|
|||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -5151,6 +5166,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -5170,6 +5186,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -5532,7 +5549,8 @@
|
|||
"version": "0.180.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
|
|
@ -5597,6 +5615,7 @@
|
|||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -5682,6 +5701,7 @@
|
|||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
|
|
@ -5697,6 +5717,7 @@
|
|||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -5818,6 +5839,7 @@
|
|||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,27 +65,36 @@ export const en: Translations = {
|
|||
},
|
||||
|
||||
status: {
|
||||
actionFailed: "Action failed",
|
||||
actionFinished: "Finished",
|
||||
actions: "Actions",
|
||||
agent: "Agent",
|
||||
gateway: "Gateway",
|
||||
activeSessions: "Active Sessions",
|
||||
recentSessions: "Recent Sessions",
|
||||
connectedPlatforms: "Connected Platforms",
|
||||
running: "Running",
|
||||
starting: "Starting",
|
||||
failed: "Failed",
|
||||
stopped: "Stopped",
|
||||
connected: "Connected",
|
||||
connectedPlatforms: "Connected Platforms",
|
||||
disconnected: "Disconnected",
|
||||
error: "Error",
|
||||
notRunning: "Not running",
|
||||
startFailed: "Start failed",
|
||||
pid: "PID",
|
||||
runningRemote: "Running (remote)",
|
||||
noneRunning: "None",
|
||||
failed: "Failed",
|
||||
gateway: "Gateway",
|
||||
gatewayFailedToStart: "Gateway failed to start",
|
||||
lastUpdate: "Last update",
|
||||
platformError: "error",
|
||||
noneRunning: "None",
|
||||
notRunning: "Not running",
|
||||
pid: "PID",
|
||||
platformDisconnected: "disconnected",
|
||||
platformError: "error",
|
||||
recentSessions: "Recent Sessions",
|
||||
restartGateway: "Restart Gateway",
|
||||
restartingGateway: "Restarting gateway…",
|
||||
running: "Running",
|
||||
runningRemote: "Running (remote)",
|
||||
startFailed: "Start failed",
|
||||
starting: "Starting",
|
||||
startedInBackground: "Started in background — check logs for progress",
|
||||
stopped: "Stopped",
|
||||
updateHermes: "Update Hermes",
|
||||
updatingHermes: "Updating Hermes…",
|
||||
waitingForOutput: "Waiting for output…",
|
||||
},
|
||||
|
||||
sessions: {
|
||||
|
|
|
|||
|
|
@ -68,27 +68,36 @@ export interface Translations {
|
|||
|
||||
// ── Status page ──
|
||||
status: {
|
||||
actionFailed: string;
|
||||
actionFinished: string;
|
||||
actions: string;
|
||||
agent: string;
|
||||
gateway: string;
|
||||
activeSessions: string;
|
||||
recentSessions: string;
|
||||
connectedPlatforms: string;
|
||||
running: string;
|
||||
starting: string;
|
||||
failed: string;
|
||||
stopped: string;
|
||||
connected: string;
|
||||
connectedPlatforms: string;
|
||||
disconnected: string;
|
||||
error: string;
|
||||
notRunning: string;
|
||||
startFailed: string;
|
||||
pid: string;
|
||||
runningRemote: string;
|
||||
noneRunning: string;
|
||||
failed: string;
|
||||
gateway: string;
|
||||
gatewayFailedToStart: string;
|
||||
lastUpdate: string;
|
||||
platformError: string;
|
||||
noneRunning: string;
|
||||
notRunning: string;
|
||||
pid: string;
|
||||
platformDisconnected: string;
|
||||
platformError: string;
|
||||
activeSessions: string;
|
||||
recentSessions: string;
|
||||
restartGateway: string;
|
||||
restartingGateway: string;
|
||||
running: string;
|
||||
runningRemote: string;
|
||||
startFailed: string;
|
||||
starting: string;
|
||||
startedInBackground: string;
|
||||
stopped: string;
|
||||
updateHermes: string;
|
||||
updatingHermes: string;
|
||||
waitingForOutput: string;
|
||||
};
|
||||
|
||||
// ── Sessions page ──
|
||||
|
|
|
|||
|
|
@ -65,27 +65,36 @@ export const zh: Translations = {
|
|||
},
|
||||
|
||||
status: {
|
||||
actionFailed: "操作失败",
|
||||
actionFinished: "已完成",
|
||||
actions: "操作",
|
||||
agent: "代理",
|
||||
gateway: "网关",
|
||||
activeSessions: "活跃会话",
|
||||
recentSessions: "最近会话",
|
||||
connectedPlatforms: "已连接平台",
|
||||
running: "运行中",
|
||||
starting: "启动中",
|
||||
failed: "失败",
|
||||
stopped: "已停止",
|
||||
connected: "已连接",
|
||||
connectedPlatforms: "已连接平台",
|
||||
disconnected: "已断开",
|
||||
error: "错误",
|
||||
notRunning: "未运行",
|
||||
startFailed: "启动失败",
|
||||
pid: "进程",
|
||||
runningRemote: "运行中(远程)",
|
||||
noneRunning: "无",
|
||||
failed: "失败",
|
||||
gateway: "网关",
|
||||
gatewayFailedToStart: "网关启动失败",
|
||||
lastUpdate: "最后更新",
|
||||
platformError: "错误",
|
||||
noneRunning: "无",
|
||||
notRunning: "未运行",
|
||||
pid: "进程",
|
||||
platformDisconnected: "已断开",
|
||||
platformError: "错误",
|
||||
recentSessions: "最近会话",
|
||||
restartGateway: "重启网关",
|
||||
restartingGateway: "正在重启网关…",
|
||||
running: "运行中",
|
||||
runningRemote: "运行中(远程)",
|
||||
startFailed: "启动失败",
|
||||
starting: "启动中",
|
||||
startedInBackground: "已在后台启动 — 请查看日志",
|
||||
stopped: "已停止",
|
||||
updateHermes: "更新 Hermes",
|
||||
updatingHermes: "正在更新 Hermes…",
|
||||
waitingForOutput: "等待输出…",
|
||||
},
|
||||
|
||||
sessions: {
|
||||
|
|
|
|||
|
|
@ -183,6 +183,16 @@ export const api = {
|
|||
);
|
||||
},
|
||||
|
||||
// Gateway / update actions
|
||||
restartGateway: () =>
|
||||
fetchJSON<ActionResponse>("/api/gateway/restart", { method: "POST" }),
|
||||
updateHermes: () =>
|
||||
fetchJSON<ActionResponse>("/api/hermes/update", { method: "POST" }),
|
||||
getActionStatus: (name: string, lines = 200) =>
|
||||
fetchJSON<ActionStatusResponse>(
|
||||
`/api/actions/${encodeURIComponent(name)}/status?lines=${lines}`,
|
||||
),
|
||||
|
||||
// Dashboard plugins
|
||||
getPlugins: () =>
|
||||
fetchJSON<PluginManifestResponse[]>("/api/dashboard/plugins"),
|
||||
|
|
@ -200,6 +210,20 @@ export const api = {
|
|||
}),
|
||||
};
|
||||
|
||||
export interface ActionResponse {
|
||||
name: string;
|
||||
ok: boolean;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
export interface ActionStatusResponse {
|
||||
exit_code: number | null;
|
||||
lines: string[];
|
||||
name: string;
|
||||
pid: number | null;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export interface PlatformStatus {
|
||||
error_code?: string;
|
||||
error_message?: string;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,53 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Cpu,
|
||||
Database,
|
||||
Download,
|
||||
Loader2,
|
||||
Radio,
|
||||
RotateCw,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Cell, Grid } from "@nous-research/ui";
|
||||
import { api } from "@/lib/api";
|
||||
import type { PlatformStatus, SessionInfo, StatusResponse } from "@/lib/api";
|
||||
import { timeAgo, isoTimeAgo } from "@/lib/utils";
|
||||
import type {
|
||||
ActionStatusResponse,
|
||||
PlatformStatus,
|
||||
SessionInfo,
|
||||
StatusResponse,
|
||||
} from "@/lib/api";
|
||||
import { cn, timeAgo, isoTimeAgo } from "@/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Toast } from "@/components/Toast";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
const ACTION_NAMES: Record<"restart" | "update", string> = {
|
||||
restart: "gateway-restart",
|
||||
update: "hermes-update",
|
||||
};
|
||||
|
||||
export default function StatusPage() {
|
||||
const [status, setStatus] = useState<StatusResponse | null>(null);
|
||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||
const [pendingAction, setPendingAction] = useState<
|
||||
"restart" | "update" | null
|
||||
>(null);
|
||||
const [activeAction, setActiveAction] = useState<"restart" | "update" | null>(
|
||||
null,
|
||||
);
|
||||
const [actionStatus, setActionStatus] = useState<ActionStatusResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [toast, setToast] = useState<ToastState | null>(null);
|
||||
const logScrollRef = useRef<HTMLPreElement | null>(null);
|
||||
const { t } = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -38,6 +66,75 @@ export default function StatusPage() {
|
|||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toast) return;
|
||||
const timer = setTimeout(() => setToast(null), 4000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAction) return;
|
||||
const name = ACTION_NAMES[activeAction];
|
||||
let cancelled = false;
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const resp = await api.getActionStatus(name);
|
||||
if (cancelled) return;
|
||||
setActionStatus(resp);
|
||||
if (!resp.running) {
|
||||
const ok = resp.exit_code === 0;
|
||||
setToast({
|
||||
type: ok ? "success" : "error",
|
||||
message: ok
|
||||
? t.status.actionFinished
|
||||
: `${t.status.actionFailed} (exit ${resp.exit_code ?? "?"})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// transient fetch error; keep polling
|
||||
}
|
||||
if (!cancelled) setTimeout(poll, 1500);
|
||||
};
|
||||
|
||||
poll();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeAction, t.status.actionFinished, t.status.actionFailed]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = logScrollRef.current;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}, [actionStatus?.lines]);
|
||||
|
||||
const runAction = async (action: "restart" | "update") => {
|
||||
setPendingAction(action);
|
||||
setActionStatus(null);
|
||||
try {
|
||||
if (action === "restart") {
|
||||
await api.restartGateway();
|
||||
} else {
|
||||
await api.updateHermes();
|
||||
}
|
||||
setActiveAction(action);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
setToast({
|
||||
type: "error",
|
||||
message: `${t.status.actionFailed}: ${detail}`,
|
||||
});
|
||||
} finally {
|
||||
setPendingAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const dismissLog = () => {
|
||||
setActiveAction(null);
|
||||
setActionStatus(null);
|
||||
};
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
|
|
@ -144,6 +241,8 @@ export default function StatusPage() {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Toast toast={toast} />
|
||||
|
||||
{alerts.length > 0 && (
|
||||
<div className="border border-destructive/30 bg-destructive/[0.06] p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
|
|
@ -196,6 +295,125 @@ export default function StatusPage() {
|
|||
))}
|
||||
</Grid>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t.status.actions}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => runAction("restart")}
|
||||
disabled={
|
||||
pendingAction !== null ||
|
||||
(activeAction !== null && actionStatus?.running !== false)
|
||||
}
|
||||
>
|
||||
<RotateCw
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
(pendingAction === "restart" ||
|
||||
(activeAction === "restart" && actionStatus?.running)) &&
|
||||
"animate-spin",
|
||||
)}
|
||||
/>
|
||||
|
||||
{activeAction === "restart" && actionStatus?.running
|
||||
? t.status.restartingGateway
|
||||
: t.status.restartGateway}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => runAction("update")}
|
||||
disabled={
|
||||
pendingAction !== null ||
|
||||
(activeAction !== null && actionStatus?.running !== false)
|
||||
}
|
||||
>
|
||||
<Download
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
(pendingAction === "update" ||
|
||||
(activeAction === "update" && actionStatus?.running)) &&
|
||||
"animate-pulse",
|
||||
)}
|
||||
/>
|
||||
|
||||
{activeAction === "update" && actionStatus?.running
|
||||
? t.status.updatingHermes
|
||||
: t.status.updateHermes}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeAction && (
|
||||
<div className="border border-border bg-background-base/50">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{actionStatus?.running ? (
|
||||
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-warning" />
|
||||
) : actionStatus?.exit_code === 0 ? (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-success" />
|
||||
) : actionStatus !== null ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-destructive" />
|
||||
) : (
|
||||
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
|
||||
<span className="text-xs font-mondwest tracking-[0.12em] truncate">
|
||||
{activeAction === "restart"
|
||||
? t.status.restartGateway
|
||||
: t.status.updateHermes}
|
||||
</span>
|
||||
|
||||
<Badge
|
||||
variant={
|
||||
actionStatus?.running
|
||||
? "warning"
|
||||
: actionStatus?.exit_code === 0
|
||||
? "success"
|
||||
: actionStatus
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
className="text-[10px] shrink-0"
|
||||
>
|
||||
{actionStatus?.running
|
||||
? t.status.running
|
||||
: actionStatus?.exit_code === 0
|
||||
? t.status.actionFinished
|
||||
: actionStatus
|
||||
? `${t.status.actionFailed} (${actionStatus.exit_code ?? "?"})`
|
||||
: t.common.loading}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismissLog}
|
||||
className="shrink-0 opacity-60 hover:opacity-100 cursor-pointer"
|
||||
aria-label={t.common.close}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
ref={logScrollRef}
|
||||
className="max-h-72 overflow-auto px-3 py-2 font-mono-ui text-[11px] leading-relaxed whitespace-pre-wrap break-all"
|
||||
>
|
||||
{actionStatus?.lines && actionStatus.lines.length > 0
|
||||
? actionStatus.lines.join("\n")
|
||||
: t.status.waitingForOutput}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{platforms.length > 0 && (
|
||||
<PlatformsCard
|
||||
platforms={platforms}
|
||||
|
|
@ -378,6 +596,11 @@ function PlatformsCard({ platforms, platformStateBadge }: PlatformsCardProps) {
|
|||
);
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
}
|
||||
|
||||
interface PlatformsCardProps {
|
||||
platforms: [string, PlatformStatus][];
|
||||
platformStateBadge: Record<
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue