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 %}{{ highlight.label }}{% endraw %} + {% raw %}{{ highlight.value }}{% endraw %} +
+
+
+
+
+ {% raw %}{{ meter.label }}{% endraw %} +
+ {% raw %}{{ meter.displayValue }}{% endraw %} + + {% raw %}{{ meter.levelText }}{% 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;