diff --git a/admin/dashboard/templates/index.html b/admin/dashboard/templates/index.html
index a93d1c4..1244618 100644
--- a/admin/dashboard/templates/index.html
+++ b/admin/dashboard/templates/index.html
@@ -163,6 +163,38 @@
{% raw %}{{ getHealthStatusText(service.status) }}{% endraw %}
+
{% raw %}{{ metric.label }}{% endraw %}
@@ -606,6 +638,44 @@
if (Number.isNaN(numeric)) return String(value);
return numeric.toFixed(fractionDigits);
},
+ normalizePercent(value) {
+ const numeric = Number(value || 0);
+ if (Number.isNaN(numeric)) return 0;
+ return Math.max(0, Math.min(100, numeric));
+ },
+ buildRiskLevel(percent, warningThreshold = 50, dangerThreshold = 80) {
+ const normalized = this.normalizePercent(percent);
+ if (normalized >= dangerThreshold) {
+ return { text: '高压', className: 'danger' };
+ }
+ if (normalized >= warningThreshold) {
+ return { text: '偏高', className: 'warning' };
+ }
+ return { text: '平稳', className: 'healthy' };
+ },
+ buildPositiveLevel(percent, warningThreshold = 70, healthyThreshold = 90) {
+ const normalized = this.normalizePercent(percent);
+ if (normalized >= healthyThreshold) {
+ return { text: '优秀', className: 'healthy' };
+ }
+ if (normalized >= warningThreshold) {
+ return { text: '可接受', className: 'warning' };
+ }
+ return { text: '偏低', className: 'danger' };
+ },
+ buildMeter(label, percent, displayValue, levelBuilder) {
+ const normalized = this.normalizePercent(percent);
+ const level = typeof levelBuilder === 'function'
+ ? levelBuilder(normalized)
+ : { text: '', className: '' };
+ return {
+ label,
+ percent: normalized,
+ displayValue,
+ levelText: level.text || '',
+ levelClass: level.className || ''
+ };
+ },
countHealthyInfrastructureServices(infrastructure) {
const mysql = infrastructure.mysql || {};
const redis = infrastructure.redis || {};
@@ -623,8 +693,32 @@
title: 'MySQL',
status: mysql.status || 'warning',
summary: mysql.summary || '暂无状态',
+ highlights: [
+ {
+ label: '数据库',
+ value: mysql.database || '-',
+ tone: 'neutral'
+ },
+ {
+ label: '版本',
+ value: mysql.version || '-',
+ tone: 'neutral'
+ },
+ {
+ label: '慢SQL阈值',
+ value: `${this.formatMetricNumber(mysql.slow_query_threshold_ms)} ms`,
+ tone: 'info'
+ }
+ ],
+ meters: [
+ this.buildMeter(
+ '连接负载',
+ mysql.connection_usage_percent,
+ `${this.formatMetricNumber(mysql.connection_usage_percent, 1)}%`,
+ (percent) => this.buildRiskLevel(percent, 45, 80)
+ )
+ ],
metrics: [
- { label: '连接负载', value: `${this.formatMetricNumber(mysql.connection_usage_percent, 1)}%` },
{ label: '连接数', value: `${this.formatMetricNumber(mysql.threads_connected)} / ${mysql.max_connections || '-'}` },
{ label: '运行线程', value: this.formatMetricNumber(mysql.threads_running) },
{ label: 'QPS', value: this.formatMetricNumber(mysql.questions_per_second, 2) },
@@ -637,12 +731,42 @@
title: 'Redis',
status: redis.status || 'warning',
summary: redis.summary || '暂无状态',
+ highlights: [
+ {
+ label: 'DB',
+ value: redis.db_index ?? '-',
+ tone: 'neutral'
+ },
+ {
+ label: '峰值内存',
+ value: redis.used_memory_peak_human || '-',
+ tone: 'neutral'
+ },
+ {
+ label: '阻塞客户端',
+ value: this.formatMetricNumber(redis.blocked_clients),
+ tone: Number(redis.blocked_clients || 0) > 0 ? 'warning' : 'healthy'
+ }
+ ],
+ meters: [
+ this.buildMeter(
+ '内存占用',
+ redis.memory_usage_percent,
+ `${this.formatMetricNumber(redis.memory_usage_percent, 1)}%`,
+ (percent) => this.buildRiskLevel(percent, 60, 80)
+ ),
+ this.buildMeter(
+ '命中率',
+ redis.hit_rate_percent,
+ `${this.formatMetricNumber(redis.hit_rate_percent, 1)}%`,
+ (percent) => this.buildPositiveLevel(percent, 70, 90)
+ )
+ ],
metrics: [
{ label: 'Key 数量', value: this.formatMetricNumber(redis.key_count) },
{ label: '客户端', value: this.formatMetricNumber(redis.connected_clients) },
{ label: 'OPS/s', value: this.formatMetricNumber(redis.ops_per_sec) },
{ label: '内存占用', value: redis.used_memory_human || '-' },
- { label: '命中率', value: `${this.formatMetricNumber(redis.hit_rate_percent, 1)}%` },
{ label: '运行时间', value: this.formatCompactDuration(redis.uptime_seconds) }
]
}
@@ -1320,6 +1444,143 @@
color: #64748b;
}
+ .health-service-highlights {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-bottom: 12px;
+ }
+
+ .health-service-highlight {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(148, 163, 184, 0.16);
+ color: #475569;
+ font-size: 11px;
+ }
+
+ .health-service-highlight--healthy {
+ color: #047857;
+ background: rgba(16, 185, 129, 0.10);
+ border-color: rgba(16, 185, 129, 0.16);
+ }
+
+ .health-service-highlight--warning {
+ color: #b45309;
+ background: rgba(245, 158, 11, 0.12);
+ border-color: rgba(245, 158, 11, 0.16);
+ }
+
+ .health-service-highlight--info {
+ color: #1d4ed8;
+ background: rgba(59, 130, 246, 0.10);
+ border-color: rgba(59, 130, 246, 0.16);
+ }
+
+ .health-service-highlight__label {
+ color: inherit;
+ opacity: 0.86;
+ }
+
+ .health-service-highlight__value {
+ color: #0f172a;
+ font-weight: 600;
+ max-width: 180px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .health-service-meter-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-bottom: 12px;
+ }
+
+ .health-service-meter__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 6px;
+ }
+
+ .health-service-meter__label {
+ font-size: 11px;
+ color: #64748b;
+ }
+
+ .health-service-meter__meta {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .health-service-meter__number {
+ font-size: 12px;
+ color: #0f172a;
+ font-weight: 700;
+ }
+
+ .health-service-meter__badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 3px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 700;
+ background: rgba(148, 163, 184, 0.12);
+ color: #64748b;
+ }
+
+ .health-service-meter__badge--healthy {
+ color: #047857;
+ background: rgba(16, 185, 129, 0.12);
+ }
+
+ .health-service-meter__badge--warning {
+ color: #b45309;
+ background: rgba(245, 158, 11, 0.14);
+ }
+
+ .health-service-meter__badge--danger {
+ color: #b91c1c;
+ background: rgba(239, 68, 68, 0.14);
+ }
+
+ .health-service-meter__track {
+ width: 100%;
+ height: 8px;
+ border-radius: 999px;
+ background: rgba(226, 232, 240, 0.88);
+ overflow: hidden;
+ }
+
+ .health-service-meter__fill {
+ height: 100%;
+ border-radius: 999px;
+ background: linear-gradient(90deg, rgba(16, 185, 129, 0.68), rgba(5, 150, 105, 0.92));
+ }
+
+ .health-service-meter__fill--healthy {
+ background: linear-gradient(90deg, rgba(16, 185, 129, 0.68), rgba(5, 150, 105, 0.92));
+ }
+
+ .health-service-meter__fill--warning {
+ background: linear-gradient(90deg, rgba(245, 158, 11, 0.70), rgba(217, 119, 6, 0.95));
+ }
+
+ .health-service-meter__fill--danger {
+ background: linear-gradient(90deg, rgba(239, 68, 68, 0.72), rgba(220, 38, 38, 0.96));
+ }
+
.health-service-panel__badge {
display: inline-flex;
align-items: center;