hy-app-monitor/static/index.html
2026-04-22 19:21:43 +08:00

852 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>HY App Monitor</title>
<link rel="stylesheet" href="/app.css" />
<script defer src="/vue.js"></script>
<script defer src="/app.js"></script>
</head>
<body>
<div id="boot-indicator" class="boot-indicator">加载中...</div>
<noscript>
<div class="boot-indicator">请启用 JavaScript</div>
</noscript>
<div id="app" v-cloak class="shell">
<header class="hero">
<div class="hero-copy">
<p class="eyebrow">HY APP MONITOR</p>
<h1>服务 / 主机 / Nacos / MongoDB</h1>
</div>
<div class="hero-side">
<div class="stat-block">
<span>服务总数</span>
<strong>{{ summary.total }}</strong>
</div>
<div class="stat-block ok">
<span>服务正常</span>
<strong>{{ summary.ok }}</strong>
</div>
<div class="stat-block bad">
<span>服务异常</span>
<strong>{{ summary.down }}</strong>
</div>
<div class="stat-block neutral">
<span>主机数</span>
<strong>{{ summary.hostTotal }}</strong>
</div>
<div class="stat-block neutral">
<span>Nacos 服务</span>
<strong>{{ summary.nacosServiceTotal }}</strong>
</div>
<div :class="['stat-block', summary.nacosUnhealthyInstanceTotal > 0 ? 'bad' : 'ok']">
<span>Nacos 异常实例</span>
<strong>{{ summary.nacosUnhealthyInstanceTotal }}</strong>
</div>
<div :class="['stat-block', payload.mongo && payload.mongo.ok ? 'ok' : 'bad']">
<span>Mongo 状态</span>
<strong>{{ payload.mongo && payload.mongo.ok ? 'OK' : 'ERR' }}</strong>
</div>
<div class="stat-block neutral">
<span>Mongo 集合</span>
<strong>{{ summary.mongoCollectionTotal }}</strong>
</div>
</div>
</header>
<section class="toolbar">
<div class="toolbar-left">
<button :class="['filter-chip', filter === 'all' ? 'active' : '']" @click="filter = 'all'">全部</button>
<button :class="['filter-chip', filter === 'down' ? 'active' : '']" @click="filter = 'down'">异常</button>
<button :class="['filter-chip', filter === 'java' ? 'active' : '']" @click="filter = 'java'">Java</button>
<button :class="['filter-chip', filter === 'golang' ? 'active' : '']" @click="filter = 'golang'">Golang</button>
</div>
<div class="toolbar-right">
<input v-model.trim="keyword" class="search" placeholder="搜索主机 / 服务 / IP / Nacos" />
<label class="interval-field">
<span>刷新</span>
<select v-model.number="refreshIntervalSeconds" class="interval-select">
<option v-for="seconds in refreshIntervalOptions" :key="seconds" :value="seconds">{{ seconds }}s</option>
</select>
</label>
<button class="refresh" @click="refresh" :disabled="loading">{{ loading ? '刷新中...' : '立即刷新' }}</button>
</div>
</section>
<section class="meta-strip">
<span>最后刷新:{{ formattedUpdatedAt }}</span>
<span>主机指标正常:{{ summary.hostMetricOk }} / {{ summary.hostTotal }}</span>
<span>Nacos 实例:{{ summary.nacosInstanceTotal }}</span>
<span>Mongo 数据库:{{ summary.mongoDatabaseTotal }}</span>
<span v-if="error" class="bad-text">加载失败:{{ error }}</span>
</section>
<section class="section-head">
<h2>主机与服务</h2>
</section>
<section class="hosts">
<article v-for="group in groupedHosts" :key="group.host" class="host-panel">
<header class="host-header">
<div class="host-identity">
<h2>{{ group.host }}</h2>
<p>{{ group.ip }} · {{ group.groupLabel }}</p>
</div>
<div class="host-badges">
<span class="badge neutral">{{ group.serviceSummary.total }} 项服务</span>
<span :class="['badge', group.downCount > 0 ? 'bad' : 'ok']">
{{ group.downCount > 0 ? `${group.downCount} 异常` : '服务正常' }}
</span>
<span :class="['badge', group.metrics && group.metrics.ok ? 'ok' : 'bad']">
{{ group.metrics && group.metrics.ok ? '主机指标正常' : '主机指标异常' }}
</span>
</div>
</header>
<section class="host-metrics">
<div :class="['metric-card', toneClass(group.metrics.cpuLevel)]">
<small>CPU</small>
<strong>{{ formatPercent(group.metrics.cpuPercent) }}</strong>
</div>
<div :class="['metric-card', toneClass(group.metrics.memoryLevel)]">
<small>内存</small>
<strong>{{ formatPercent(group.metrics.memoryPercent) }}</strong>
<span>{{ formatGb(group.metrics.memoryUsedGb, group.metrics.memoryTotalGb) }}</span>
</div>
<div :class="['metric-card', toneClass(group.metrics.diskLevel)]">
<small>磁盘</small>
<strong>{{ formatPercent(group.metrics.diskPercent) }}</strong>
<span>{{ formatGb(group.metrics.diskUsedGb, group.metrics.diskTotalGb) }}</span>
</div>
<div :class="['metric-card', toneClass(group.metrics.processLevel)]">
<small>进程数</small>
<strong>{{ group.metrics.processCount ?? '-' }}</strong>
<span>{{ group.metrics.hostname || group.instanceId }}</span>
</div>
</section>
<p v-if="group.metrics && group.metrics.error" class="metric-error">{{ group.metrics.error }}</p>
<div v-if="group.items.length > 0" class="service-grid">
<div v-for="item in group.items" :key="item.host + '-' + item.service" class="service-card">
<div class="service-top">
<div>
<div class="service-name">{{ item.service }}</div>
<div class="service-kind">{{ item.kind }}</div>
</div>
<div :class="['status-pill', toneClass(item.level)]">
<span class="dot"></span>
{{ item.ok ? 'UP' : 'DOWN' }}
</div>
</div>
<div class="service-data">
<div>
<small>HTTP</small>
<strong>{{ item.statusCode || '-' }}</strong>
</div>
<div>
<small>耗时</small>
<strong>{{ item.latencyMs }}ms</strong>
</div>
<div>
<small>端口</small>
<strong>{{ item.port }}</strong>
</div>
</div>
<div class="service-url">{{ item.url }}</div>
<div class="service-detail">{{ item.detail || '无返回内容' }}</div>
</div>
</div>
</article>
</section>
<section class="section-head">
<h2>Nacos 注册</h2>
</section>
<section class="infra-panel">
<div class="panel-head">
<div>
<h3>Nacos</h3>
<p>{{ payload.nacos.baseUrl }} · {{ payload.nacos.namespaceName }} · {{ payload.nacos.namespaceId }}</p>
</div>
<span :class="['badge', payload.nacos && payload.nacos.ok ? 'ok' : 'bad']">
{{ payload.nacos && payload.nacos.ok ? '连接正常' : '连接异常' }}
</span>
</div>
<div class="summary-grid">
<div class="summary-card">
<small>命名空间</small>
<strong>{{ payload.nacos.summary.namespaceTotal || 0 }}</strong>
</div>
<div class="summary-card">
<small>服务数</small>
<strong>{{ payload.nacos.summary.serviceTotal || 0 }}</strong>
</div>
<div class="summary-card">
<small>实例数</small>
<strong>{{ payload.nacos.summary.instanceTotal || 0 }}</strong>
</div>
<div :class="['summary-card', (payload.nacos.summary.unhealthyInstanceTotal || 0) > 0 ? 'tone-bad' : 'tone-ok']">
<small>异常实例</small>
<strong>{{ payload.nacos.summary.unhealthyInstanceTotal || 0 }}</strong>
</div>
</div>
<p v-if="payload.nacos.error" class="metric-error">{{ payload.nacos.error }}</p>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>命名空间</th>
<th>ID</th>
<th>配置数</th>
<th>类型</th>
</tr>
</thead>
<tbody>
<tr v-for="item in payload.nacos.namespaces" :key="item.namespaceId">
<td>{{ item.name }}</td>
<td class="mono">{{ item.namespaceId }}</td>
<td>{{ item.configCount }}</td>
<td>{{ item.type }}</td>
</tr>
</tbody>
</table>
</div>
<div class="nacos-service-grid">
<article v-for="service in filteredNacosServices" :key="service.rawServiceName" class="nacos-service-card">
<header class="nacos-service-head">
<div>
<h4>{{ service.serviceName }}</h4>
<p>{{ service.namespaceName || 'public' }} · {{ service.groupName }}</p>
</div>
<span :class="['badge', service.level === 'ok' ? 'ok' : service.level === 'warn' ? 'warn' : 'bad']">
{{ service.instanceTotal }} / {{ service.healthyTotal }}
</span>
</header>
<div class="table-wrap compact">
<table class="table compact">
<thead>
<tr>
<th>IP</th>
<th>端口</th>
<th>集群</th>
<th>健康</th>
<th>启用</th>
<th>权重</th>
</tr>
</thead>
<tbody>
<tr v-for="instance in service.instances" :key="service.rawServiceName + '-' + instance.ip + '-' + instance.port">
<td class="mono">{{ instance.ip }}</td>
<td>{{ instance.port }}</td>
<td>{{ instance.clusterName }}</td>
<td :class="instance.healthy ? 'ok-text' : 'bad-text'">{{ instance.healthy ? 'Y' : 'N' }}</td>
<td :class="instance.enabled ? 'ok-text' : 'bad-text'">{{ instance.enabled ? 'Y' : 'N' }}</td>
<td>{{ instance.weight }}</td>
</tr>
</tbody>
</table>
</div>
</article>
</div>
</section>
<section class="section-head">
<h2>Nacos 配置</h2>
</section>
<section class="infra-panel">
<div class="panel-head">
<div>
<h3>Nacos Config</h3>
<p>读取当前命名空间下的配置并直接编辑保存</p>
</div>
<span :class="['badge', nacosConfig.error || nacosConfig.detailError ? 'bad' : 'ok']">
{{ nacosConfig.error || nacosConfig.detailError ? '存在错误' : '可操作' }}
</span>
</div>
<div class="config-toolbar">
<input
v-model.trim="nacosConfig.keyword"
class="search config-search"
placeholder="搜索 dataId / group / appName"
/>
<button class="refresh" @click="refreshNacosConfigs({ preserveSelection: true, reloadSelected: false })" :disabled="nacosConfig.loading">
{{ nacosConfig.loading ? '加载中...' : '刷新配置列表' }}
</button>
</div>
<p v-if="nacosConfig.error" class="metric-error">{{ nacosConfig.error }}</p>
<p v-if="nacosConfig.detailError" class="metric-error">{{ nacosConfig.detailError }}</p>
<div class="config-layout">
<aside class="config-list">
<button
v-for="item in filteredNacosConfigs"
:key="configKey(item.group, item.dataId)"
:class="['config-item', configKey(item.group, item.dataId) === nacosConfig.selectedKey ? 'active' : '']"
@click="openNacosConfig(item)"
>
<strong>{{ item.dataId }}</strong>
<span>{{ item.group }} · {{ item.type || 'text' }}</span>
<small v-if="item.appName">{{ item.appName }}</small>
</button>
<div v-if="!nacosConfig.loading && filteredNacosConfigs.length === 0" class="config-empty">
当前没有可展示的配置项
</div>
</aside>
<section class="config-editor">
<template v-if="nacosConfig.editor.dataId">
<div class="config-meta-grid">
<div class="summary-card">
<small>Data ID</small>
<strong class="mono">{{ nacosConfig.editor.dataId }}</strong>
</div>
<div class="summary-card">
<small>Group</small>
<strong class="mono">{{ nacosConfig.editor.group }}</strong>
</div>
<div class="summary-card">
<small>Type</small>
<strong>{{ nacosConfig.editor.type || 'text' }}</strong>
</div>
<div class="summary-card">
<small>最后修改</small>
<strong>{{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</strong>
</div>
</div>
<p v-if="nacosConfig.editor.description" class="config-description">{{ nacosConfig.editor.description }}</p>
<p v-if="nacosConfig.editor.appName" class="config-description">appName{{ nacosConfig.editor.appName }}</p>
<p v-if="nacosConfig.editor.md5" class="config-description mono">md5{{ nacosConfig.editor.md5 }}</p>
<textarea
v-model="nacosConfig.editor.content"
class="config-textarea mono"
spellcheck="false"
:disabled="nacosConfig.detailLoading || nacosConfig.saving"
></textarea>
<div class="config-actions">
<span v-if="nacosConfig.saveMessage" class="ok-text">{{ nacosConfig.saveMessage }}</span>
<span v-else-if="nacosConfig.validationMessage" class="ok-text">{{ nacosConfig.validationMessage }}</span>
<span v-else-if="nacosConfig.detailLoading" class="badge neutral">配置加载中</span>
<span v-else-if="nacosConfigDirty" class="badge warn">有未保存修改</span>
<span v-else class="badge neutral">内容已同步</span>
<div class="action-group">
<button class="refresh ghost" @click="validateNacosConfig" :disabled="nacosConfig.validating || nacosConfig.saving || nacosConfig.detailLoading">
{{ nacosConfig.validating ? '校验中...' : '校验格式' }}
</button>
<button class="refresh" @click="saveNacosConfig" :disabled="nacosConfig.saving || nacosConfig.detailLoading">
{{ nacosConfig.saving ? '保存中...' : '保存配置' }}
</button>
</div>
</div>
</template>
<div v-else class="config-empty">
请选择左侧配置项
</div>
</section>
</div>
</section>
<section class="section-head">
<h2>Golang 配置</h2>
</section>
<section class="infra-panel">
<div class="panel-head">
<div>
<h3>Golang Runtime Env</h3>
<p>读取 app 节点当前运行中的 service.env只暴露 chatapp3-golang 实际消费的环境变量</p>
</div>
<span :class="['badge', goConfig.error ? 'bad' : goConfig.diffKeys.length > 0 ? 'warn' : 'ok']">
{{ goConfig.error ? '存在错误' : goConfig.diffKeys.length > 0 ? '双机有差异' : '已同步' }}
</span>
</div>
<div class="config-toolbar">
<div class="host-tags">
<span v-for="host in goConfig.hosts" :key="host.host" class="badge neutral">
{{ host.host }} · {{ host.path }}
</span>
</div>
<button class="refresh" @click="refreshGoConfig" :disabled="goConfig.loading">
{{ goConfig.loading ? '加载中...' : '刷新 Go 配置' }}
</button>
</div>
<p v-if="goConfig.error" class="metric-error">{{ goConfig.error }}</p>
<p v-if="goConfig.diffKeys.length > 0" class="metric-error">
当前 app 节点存在差异 key{{ goConfig.diffKeys.join(', ') }}
</p>
<section class="config-editor">
<textarea
v-model="goConfig.editorContent"
class="config-textarea mono"
spellcheck="false"
:disabled="goConfig.loading || goConfig.saving"
></textarea>
<div class="config-actions">
<span v-if="goConfig.saveMessage" class="ok-text">{{ goConfig.saveMessage }}</span>
<span v-else-if="goConfig.validationMessage" class="ok-text">{{ goConfig.validationMessage }}</span>
<span v-else-if="goConfigDirty" class="badge warn">有未保存修改</span>
<span v-else class="badge neutral">内容已同步</span>
<div class="action-group">
<button class="refresh ghost" @click="validateGoConfig" :disabled="goConfig.validating || goConfig.saving || goConfig.loading">
{{ goConfig.validating ? '校验中...' : '校验格式' }}
</button>
<button class="refresh" @click="saveGoConfig" :disabled="goConfig.saving || goConfig.loading">
{{ goConfig.saving ? '保存中...' : '保存 Go 配置' }}
</button>
</div>
</div>
</section>
</section>
<section class="section-head">
<h2>服务更新</h2>
</section>
<section class="infra-panel">
<div class="panel-head">
<div>
<h3>Service Update</h3>
<p>本机构建选中的 Java / Golang 服务,推到 Harbor更新完整 compose并只重建你选中的服务</p>
</div>
<span :class="['badge', updateStatusBadgeClass()]">
{{ updateStatusLabel() }}
</span>
</div>
<div class="config-toolbar">
<div class="restart-pills">
<button
v-for="item in updateOps.items"
:key="item.service"
:class="['config-item', updateOps.selectedServices.includes(item.service) ? 'active' : '']"
@click="toggleUpdateService(item.service)"
>
<strong>{{ item.service }}</strong>
<span>{{ item.kind }} · {{ item.hosts.map((host) => host.host).join(' / ') }}</span>
<small v-if="item.currentImages && item.currentImages.length > 0" class="mono">
{{ item.currentImages.length === 1 ? item.currentImages[0] : `${item.currentImages.length} 个当前镜像` }}
</small>
</button>
</div>
<div class="action-group">
<button class="refresh ghost" @click="refreshUpdateTargets" :disabled="updateOps.loading || updateOps.running">
{{ updateOps.loading ? '加载中...' : '刷新更新目标' }}
</button>
<button class="refresh danger" @click="runServiceUpdate" :disabled="updateOps.running || updateOps.selectedServices.length === 0">
{{ updateOps.running ? '更新中...' : `更新 ${updateOps.selectedServices.length} 项服务` }}
</button>
</div>
</div>
<div class="host-tags">
<span class="badge neutral mono">
Harbor{{ updateOps.builder.harborRegistry }} / {{ updateOps.builder.harborProject }}
</span>
<span class="badge neutral mono">Java 仓:{{ updateOps.builder.javaRepoRoot }}</span>
<span class="badge neutral mono">Go 仓:{{ updateOps.builder.golangRepoRoot }}</span>
<span class="badge neutral">平台:{{ updateOps.defaults.dockerPlatform }}</span>
</div>
<div class="update-form-grid">
<label class="update-field">
<span>Java Git Ref</span>
<input v-model.trim="updateOps.javaGitRef" class="search" placeholder="main / branch / tag / commit" />
</label>
<label class="update-field">
<span>Go Git Ref</span>
<input v-model.trim="updateOps.goGitRef" class="search" placeholder="main / branch / tag / commit" />
</label>
<label class="update-field">
<span>Image Tag</span>
<input v-model.trim="updateOps.imageTag" class="search" placeholder="可选,不填则自动生成" />
</label>
<label class="update-check">
<input v-model="updateOps.skipMavenBuild" type="checkbox" />
<span>复用已有 Java Jar跳过 Maven</span>
</label>
</div>
<p v-if="updateOps.error" class="metric-error">{{ updateOps.error }}</p>
<p v-if="updateOps.message" :class="updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p>
<div v-if="updateOps.operationId" class="host-tags operation-meta">
<span class="badge neutral mono">任务:{{ updateOps.operationId }}</span>
<span :class="['badge', updateStatusBadgeClass()]">状态:{{ updateStatusLabel() }}</span>
<span class="badge neutral mono">阶段:{{ updateOps.stage || '-' }}</span>
<span class="badge neutral">开始:{{ formatDateTime(updateOps.startedAt) }}</span>
<span v-if="updateOps.finishedAt" class="badge neutral">结束:{{ formatDateTime(updateOps.finishedAt) }}</span>
</div>
<section v-if="updateOps.operationId || updateOps.running || updateOps.logs.length > 0" class="update-log-panel">
<div class="panel-head update-log-head">
<div>
<h3>Update Log</h3>
<p>按阶段展示构建、推 Harbor、同步 compose、预拉镜像和滚动重建的实时输出可下载完整日志</p>
</div>
<div class="action-group">
<button class="refresh ghost" @click="fetchUpdateOperationStatus(updateOps.operationId)" :disabled="!updateOps.operationId || updateOps.running">
刷新日志
</button>
<a
v-if="updateOps.operationId"
class="refresh ghost"
:href="updateLogDownloadUrl()"
target="_blank"
rel="noopener"
download
>
下载完整日志
</a>
</div>
</div>
<pre id="service-update-log-output" class="update-log-output mono">{{ updateLogText || '等待更新日志...' }}</pre>
</section>
<div v-if="updateOps.result && updateOps.result.builds && updateOps.result.builds.length > 0" class="table-wrap">
<table class="table compact">
<thead>
<tr>
<th>仓库类型</th>
<th>Git Ref</th>
<th>Commit</th>
<th>Image Tag</th>
<th>服务</th>
</tr>
</thead>
<tbody>
<tr v-for="build in updateOps.result.builds" :key="build.kind + '-' + build.commit">
<td>{{ build.kind }}</td>
<td class="mono">{{ build.gitRef }}</td>
<td class="mono">{{ build.commit }}</td>
<td class="mono">{{ build.imageTag }}</td>
<td>{{ (build.services || []).join(', ') }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="updateOps.result && updateOps.result.preparedHosts && updateOps.result.preparedHosts.length > 0" class="table-wrap">
<table class="table compact">
<thead>
<tr>
<th>主机</th>
<th>服务</th>
<th>Compose</th>
<th>预拉镜像</th>
</tr>
</thead>
<tbody>
<tr v-for="item in updateOps.result.preparedHosts" :key="item.host">
<td>{{ item.host }}</td>
<td>{{ (item.services || []).join(', ') }}</td>
<td>{{ item.compose && item.compose.changed ? '已更新' : '未变化' }}</td>
<td :class="item.prepull && item.prepull.status === 'SUCCESS' ? 'ok-text' : 'bad-text'">
{{ item.prepull ? item.prepull.status : '-' }}
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="updateOps.result && updateOps.result.steps && updateOps.result.steps.length > 0" class="table-wrap">
<table class="table compact">
<thead>
<tr>
<th>服务</th>
<th>主机</th>
<th>镜像</th>
<th>状态</th>
<th>HTTP</th>
<th>Nacos</th>
<th>耗时</th>
</tr>
</thead>
<tbody>
<tr v-for="step in updateOps.result.steps" :key="step.service + '-' + step.host + '-' + step.image">
<td>{{ step.service }}</td>
<td>{{ step.host }}</td>
<td class="mono">{{ step.image }}</td>
<td :class="step.status === 'success' ? 'ok-text' : 'bad-text'">{{ step.status }}</td>
<td>
<span v-if="step.http">{{ step.http.statusCode }} / {{ step.http.latencyMs }}ms</span>
<span v-else>-</span>
</td>
<td>
<span v-if="step.nacos">{{ step.nacos.source || 'ok' }}</span>
<span v-else>-</span>
</td>
<td>{{ step.durationSeconds || '-' }}s</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="section-head">
<h2>滚动重启</h2>
</section>
<section class="infra-panel">
<div class="panel-head">
<div>
<h3>Rolling Restart</h3>
<p>选择要发布生效的 Java / Golang 服务,按宿主机顺序逐台重启并等待健康恢复</p>
</div>
<span :class="['badge', restartOps.error ? 'bad' : restartOps.running ? 'warn' : 'ok']">
{{ restartOps.error ? '存在错误' : restartOps.running ? '执行中' : '可操作' }}
</span>
</div>
<div class="config-toolbar">
<div class="restart-pills">
<button
v-for="item in restartOps.items"
:key="item.service"
:class="['config-item', restartOps.selectedServices.includes(item.service) ? 'active' : '']"
@click="toggleRestartService(item.service)"
>
<strong>{{ item.service }}</strong>
<span>{{ item.kind }} · {{ item.hosts.map((host) => host.host).join(' / ') }}</span>
</button>
</div>
<div class="action-group">
<button class="refresh ghost" @click="refreshRestartTargets" :disabled="restartOps.loading || restartOps.running">
{{ restartOps.loading ? '加载中...' : '刷新服务列表' }}
</button>
<button class="refresh danger" @click="runRollingRestart" :disabled="restartOps.running || restartOps.selectedServices.length === 0">
{{ restartOps.running ? '重启中...' : `滚动重启 ${restartOps.selectedServices.length} 项服务` }}
</button>
</div>
</div>
<p v-if="restartOps.error" class="metric-error">{{ restartOps.error }}</p>
<p v-if="restartOps.message" :class="restartOps.result && restartOps.result.ok ? 'ok-text' : 'metric-error'">{{ restartOps.message }}</p>
<div v-if="restartOps.result && restartOps.result.steps && restartOps.result.steps.length > 0" class="table-wrap">
<table class="table compact">
<thead>
<tr>
<th>服务</th>
<th>主机</th>
<th>类型</th>
<th>状态</th>
<th>HTTP</th>
<th>Nacos</th>
<th>耗时</th>
</tr>
</thead>
<tbody>
<tr v-for="step in restartOps.result.steps" :key="step.service + '-' + step.host">
<td>{{ step.service }}</td>
<td>{{ step.host }}</td>
<td>{{ step.kind }}</td>
<td :class="step.status === 'success' ? 'ok-text' : 'bad-text'">{{ step.status }}</td>
<td>
<span v-if="step.http">{{ step.http.statusCode }} / {{ step.http.latencyMs }}ms</span>
<span v-else>-</span>
</td>
<td>
<span v-if="step.nacos">{{ step.nacos.source || 'ok' }}</span>
<span v-else>-</span>
</td>
<td>{{ step.durationSeconds || '-' }}s</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="section-head">
<h2>MongoDB 指标</h2>
</section>
<section class="infra-panel">
<div class="panel-head">
<div>
<h3>MongoDB</h3>
<p>{{ payload.mongo.info.host || '-' }} · {{ payload.mongo.info.version || '-' }} · {{ payload.mongo.info.process || '-' }}</p>
</div>
<span :class="['badge', payload.mongo && payload.mongo.ok ? 'ok' : 'bad']">
{{ payload.mongo && payload.mongo.ok ? '连接正常' : '连接异常' }}
</span>
</div>
<div class="summary-grid">
<div class="summary-card">
<small>运行时长</small>
<strong>{{ formatDuration(payload.mongo.info.uptimeSeconds) }}</strong>
</div>
<div class="summary-card">
<small>当前连接</small>
<strong>{{ formatNumber(payload.mongo.connections.current) }}</strong>
</div>
<div class="summary-card">
<small>可用连接</small>
<strong>{{ formatNumber(payload.mongo.connections.available) }}</strong>
</div>
<div class="summary-card">
<small>数据库</small>
<strong>{{ formatNumber(payload.mongo.summary.databaseTotal) }}</strong>
</div>
<div class="summary-card">
<small>集合</small>
<strong>{{ formatNumber(payload.mongo.summary.collectionTotal) }}</strong>
</div>
<div class="summary-card">
<small>对象数</small>
<strong>{{ formatNumber(payload.mongo.summary.objectTotal) }}</strong>
</div>
<div class="summary-card">
<small>数据量</small>
<strong>{{ formatMb(payload.mongo.summary.dataSizeMb) }}</strong>
</div>
<div class="summary-card">
<small>存储量</small>
<strong>{{ formatMb(payload.mongo.summary.storageSizeMb) }}</strong>
</div>
</div>
<p v-if="payload.mongo.error" class="metric-error">{{ payload.mongo.error }}</p>
<div class="metric-grid">
<div class="summary-card">
<small>Resident</small>
<strong>{{ formatMb(payload.mongo.memory.residentMb) }}</strong>
</div>
<div class="summary-card">
<small>Virtual</small>
<strong>{{ formatMb(payload.mongo.memory.virtualMb) }}</strong>
</div>
<div class="summary-card">
<small>Mapped</small>
<strong>{{ formatMb(payload.mongo.memory.mappedMb) }}</strong>
</div>
<div class="summary-card">
<small>Requests</small>
<strong>{{ formatNumber(payload.mongo.network.numRequests) }}</strong>
</div>
<div class="summary-card">
<small>Bytes In</small>
<strong>{{ formatBytes(payload.mongo.network.bytesIn) }}</strong>
</div>
<div class="summary-card">
<small>Bytes Out</small>
<strong>{{ formatBytes(payload.mongo.network.bytesOut) }}</strong>
</div>
<div class="summary-card">
<small>WT Cache</small>
<strong>{{ formatBytes(payload.mongo.wiredTigerCache.bytesCurrentlyInCache) }}</strong>
</div>
<div class="summary-card">
<small>WT Dirty</small>
<strong>{{ formatBytes(payload.mongo.wiredTigerCache.trackedDirtyBytesInCache) }}</strong>
</div>
</div>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>操作</th>
<th>次数</th>
<th>操作</th>
<th>次数</th>
</tr>
</thead>
<tbody>
<tr>
<td>insert</td>
<td>{{ formatNumber(payload.mongo.opcounters.insert) }}</td>
<td>query</td>
<td>{{ formatNumber(payload.mongo.opcounters.query) }}</td>
</tr>
<tr>
<td>update</td>
<td>{{ formatNumber(payload.mongo.opcounters.update) }}</td>
<td>delete</td>
<td>{{ formatNumber(payload.mongo.opcounters.delete) }}</td>
</tr>
<tr>
<td>getmore</td>
<td>{{ formatNumber(payload.mongo.opcounters.getmore) }}</td>
<td>command</td>
<td>{{ formatNumber(payload.mongo.opcounters.command) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="payload.mongo.replicaSet && payload.mongo.replicaSet.configured" class="table-wrap">
<table class="table">
<thead>
<tr>
<th>成员</th>
<th>状态</th>
<th>健康</th>
<th>运行时长</th>
<th>复制延迟</th>
<th>最后 Optime</th>
</tr>
</thead>
<tbody>
<tr v-for="member in payload.mongo.replicaSet.members" :key="member.name">
<td class="mono">{{ member.name }}</td>
<td>{{ member.stateStr }}</td>
<td :class="member.health >= 1 ? 'ok-text' : 'bad-text'">{{ member.health }}</td>
<td>{{ formatDuration(member.uptime) }}</td>
<td>{{ member.lagSeconds == null ? '-' : `${member.lagSeconds}s` }}</td>
<td>{{ formatDateTime(member.optimeDate) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>数据库</th>
<th>集合</th>
<th>对象</th>
<th>数据量</th>
<th>存储量</th>
<th>索引数</th>
<th>索引量</th>
</tr>
</thead>
<tbody>
<tr v-for="db in payload.mongo.databases" :key="db.name">
<td>{{ db.name }}</td>
<td>{{ formatNumber(db.collections) }}</td>
<td>{{ formatNumber(db.objects) }}</td>
<td>{{ formatMb(db.dataSizeMb) }}</td>
<td>{{ formatMb(db.storageSizeMb) }}</td>
<td>{{ formatNumber(db.indexCount) }}</td>
<td>{{ formatMb(db.indexSizeMb) }}</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</body>
</html>