Explain gateway stopped states with Web UI diagnostics (#663)

* feat: add gateway diagnostics to Web UI status

* fix: improve gateway diagnostics mobile layout
This commit is contained in:
luSkyl
2026-05-16 21:24:48 +08:00
committed by GitHub
parent 840cb6cd9f
commit 8571a7d0ac
4 changed files with 157 additions and 2 deletions
@@ -7,6 +7,16 @@ export interface GatewayStatus {
url: string
running: boolean
pid?: number
diagnostics?: {
pid_path: string
config_path: string
pid_file_exists: boolean
config_exists: boolean
health_url: string
health_checked_at: string
health_ok?: boolean
reason: string
}
}
export async function fetchGateways(): Promise<GatewayStatus[]> {
@@ -47,6 +47,11 @@ async function handleToggle(name: string, running: boolean) {
<span class="meta-item">{{ gw.host }}:{{ gw.port }}</span>
<span v-if="gw.pid" class="meta-item">PID: {{ gw.pid }}</span>
</div>
<div v-if="gw.diagnostics" class="gateway-diagnostics">
<span class="diag-item">{{ gw.diagnostics.reason }}</span>
<span class="diag-item">PID: {{ gw.diagnostics.pid_path }}</span>
<span class="diag-item">Config: {{ gw.diagnostics.config_path }}</span>
</div>
</div>
<div class="gateway-actions">
<NTag :type="gw.running ? 'success' : 'default'" size="small" round>
@@ -99,6 +104,7 @@ async function handleToggle(name: string, running: boolean) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 20px;
background-color: $bg-card;
border: 1px solid $border-color;
@@ -110,6 +116,11 @@ async function handleToggle(name: string, running: boolean) {
}
}
.gateway-info {
min-width: 0;
flex: 1;
}
.gateway-name {
font-size: 14px;
font-weight: 600;
@@ -119,17 +130,62 @@ async function handleToggle(name: string, running: boolean) {
.gateway-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.gateway-diagnostics {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
}
.meta-item {
font-size: 12px;
color: $text-muted;
}
.diag-item {
max-width: 100%;
font-size: 12px;
color: $text-muted;
background: rgba(127, 127, 127, 0.08);
padding: 2px 8px;
border-radius: 999px;
overflow-wrap: anywhere;
line-height: 1.5;
}
.gateway-actions {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
}
@media (max-width: 640px) {
.gateways-content {
padding: 16px;
}
.gateway-card {
align-items: stretch;
flex-direction: column;
padding: 16px;
}
.gateway-diagnostics {
flex-direction: column;
gap: 6px;
}
.diag-item {
border-radius: $radius-sm;
}
.gateway-actions {
justify-content: flex-start;
}
}
</style>
@@ -131,6 +131,18 @@ export interface GatewayStatus {
url: string
running: boolean
pid?: number
diagnostics?: GatewayDiagnostics
}
export interface GatewayDiagnostics {
pid_path: string
config_path: string
pid_file_exists: boolean
config_exists: boolean
health_url: string
health_checked_at: string
health_ok?: boolean
reason: string
}
interface ManagedGateway {
@@ -508,16 +520,36 @@ export class GatewayManager {
const pid = this.readPidFile(name)
const { port, host } = this.readProfilePort(name)
const url = buildHttpUrl(host, port)
const pidPath = join(this.profileDir(name), 'gateway.pid')
const configPath = join(this.profileDir(name), 'config.yaml')
const diagnostics: GatewayDiagnostics = {
pid_path: pidPath,
config_path: configPath,
pid_file_exists: existsSync(pidPath),
config_exists: existsSync(configPath),
health_url: `${url.replace(/\/$/, '')}/health`,
health_checked_at: new Date().toISOString(),
reason: 'stopped',
}
// 首先检查 PID 文件:如果存在且进程存活且健康,则标记为运行
if (pid && this.isProcessAlive(pid) && await this.checkHealth(url)) {
diagnostics.health_ok = true
diagnostics.reason = 'pid alive and health check passed'
this.gateways.set(name, { pid, port, host, url, owned: false })
return { profile: name, port, host, url, running: true, pid }
return { profile: name, port, host, url, running: true, pid, diagnostics }
}
if (pid) {
diagnostics.health_ok = false
diagnostics.reason = this.isProcessAlive(pid) ? 'pid alive but health check failed' : 'stale pid file'
} else if (!diagnostics.pid_file_exists) {
diagnostics.reason = 'missing pid file'
}
// 没有 PID 文件时不认领端口上的未知网关,避免误判其他 profile 的网关
this.gateways.delete(name)
return { profile: name, port, host, url, running: false }
return { profile: name, port, host, url, running: false, diagnostics }
}
/** 检测所有 profile 的网关状态 */