2026-08-15 13:30:35 +08:00
|
|
|
<!DOCTYPE html>
|
2026-05-20 19:48:55 +08:00
|
|
|
<html lang="zh-CN">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>TTS 服务监控</title>
|
|
|
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
|
|
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
|
|
|
<style>
|
2026-08-15 13:30:35 +08:00
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
2026-05-20 19:48:55 +08:00
|
|
|
body {
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
2026-08-15 13:30:35 +08:00
|
|
|
min-height: 100vh; color: #e0e0e0; padding: 20px;
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
2026-08-15 13:30:35 +08:00
|
|
|
#app { max-width: 1200px; margin: 0 auto; }
|
|
|
|
|
.header { text-align: center; margin-bottom: 30px; }
|
2026-05-20 19:48:55 +08:00
|
|
|
.header h1 {
|
|
|
|
|
font-size: 2em;
|
|
|
|
|
background: linear-gradient(90deg, #00d4ff, #7b2ff7);
|
2026-08-15 13:30:35 +08:00
|
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
2026-05-20 19:48:55 +08:00
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
2026-08-15 13:30:35 +08:00
|
|
|
.header .version { color: #888; font-size: 0.9em; }
|
|
|
|
|
.actions { display: flex; justify-content: center; gap: 12px; margin-top: 15px; flex-wrap: wrap; }
|
|
|
|
|
.btn {
|
2026-05-20 19:48:55 +08:00
|
|
|
background: linear-gradient(90deg, #00d4ff, #7b2ff7);
|
2026-08-15 13:30:35 +08:00
|
|
|
border: none; color: white; padding: 10px 24px; border-radius: 8px;
|
|
|
|
|
cursor: pointer; font-size: 14px; text-decoration: none;
|
|
|
|
|
transition: opacity 0.3s; display: inline-block;
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
2026-08-15 13:30:35 +08:00
|
|
|
.btn:hover { opacity: 0.9; }
|
|
|
|
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
|
.btn.secondary {
|
|
|
|
|
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.2);
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
|
|
|
|
.grid {
|
2026-08-15 13:30:35 +08:00
|
|
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
|
|
|
gap: 20px; margin-bottom: 20px;
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
|
|
|
|
.card {
|
2026-08-15 13:30:35 +08:00
|
|
|
background: rgba(255,255,255,0.05); border-radius: 16px; padding: 24px;
|
|
|
|
|
backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1);
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
|
|
|
|
.card-title {
|
2026-08-15 13:30:35 +08:00
|
|
|
font-size: 14px; color: #888; text-transform: uppercase; letter-spacing: 1px;
|
|
|
|
|
margin-bottom: 16px; display: flex; align-items: center; gap: 8px;
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
2026-08-15 13:30:35 +08:00
|
|
|
.card-title .dot { width: 8px; height: 8px; border-radius: 50%; background: #00d4ff; }
|
|
|
|
|
.card-title .dot.error { background: #ff4757; }
|
2026-05-20 19:48:55 +08:00
|
|
|
.stat-value {
|
2026-08-15 13:30:35 +08:00
|
|
|
font-size: 2em; font-weight: bold;
|
2026-05-20 19:48:55 +08:00
|
|
|
background: linear-gradient(90deg, #00d4ff, #7b2ff7);
|
2026-08-15 13:30:35 +08:00
|
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
2026-08-15 13:30:35 +08:00
|
|
|
.stat-label { color: #888; font-size: 14px; margin-top: 5px; }
|
2026-05-20 19:48:55 +08:00
|
|
|
.info-row {
|
2026-08-15 13:30:35 +08:00
|
|
|
display: flex; justify-content: space-between;
|
|
|
|
|
padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
|
|
|
}
|
|
|
|
|
.info-row:last-child { border-bottom: none; }
|
|
|
|
|
.info-label { color: #888; }
|
|
|
|
|
.info-value { color: #fff; font-family: 'Monaco', 'Menlo', monospace; }
|
|
|
|
|
.info-value.success { color: #2ed573; }
|
|
|
|
|
.info-value.error { color: #ff4757; }
|
|
|
|
|
.memory-stat { display: flex; justify-content: space-around; text-align: center; }
|
|
|
|
|
.memory-stat .value { font-size: 1.2em; font-weight: bold; color: #00d4ff; }
|
|
|
|
|
.memory-stat .label { font-size: 12px; color: #888; margin-top: 4px; }
|
2026-05-20 19:48:55 +08:00
|
|
|
.error-box {
|
2026-08-15 13:30:35 +08:00
|
|
|
background: rgba(255,71,87,0.1); border: 1px solid rgba(255,71,87,0.3);
|
|
|
|
|
border-radius: 12px; padding: 20px; color: #ff4757; text-align: center;
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
2026-08-15 13:30:35 +08:00
|
|
|
.loading { text-align: center; padding: 40px; color: #888; }
|
|
|
|
|
.metrics-frame {
|
|
|
|
|
width: 100%; height: 600px; border: 1px solid rgba(255,255,255,0.1);
|
|
|
|
|
border-radius: 12px; background: #0e0e1a; margin-top: 20px;
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div id="app">
|
|
|
|
|
<div class="header">
|
|
|
|
|
<h1>TTS 服务监控</h1>
|
|
|
|
|
<div class="version">{{ healthData.service || 'Loading...' }} - {{ healthData.version || '' }}</div>
|
2026-08-15 13:30:35 +08:00
|
|
|
<div class="actions">
|
|
|
|
|
<button class="btn" @click="fetchHealth(true)" :disabled="loading">
|
|
|
|
|
{{ loading ? '刷新中...' : '刷新状态' }}
|
|
|
|
|
</button>
|
|
|
|
|
<a class="btn secondary" href="/metrics" target="_blank">查看 Prometheus 指标</a>
|
|
|
|
|
</div>
|
2026-05-20 19:48:55 +08:00
|
|
|
</div>
|
|
|
|
|
|
2026-08-15 13:30:35 +08:00
|
|
|
<div v-if="error" class="error-box">{{ error }}</div>
|
2026-05-20 19:48:55 +08:00
|
|
|
|
|
|
|
|
<div v-if="healthData.status">
|
|
|
|
|
<div class="grid">
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-title">
|
|
|
|
|
<span class="dot" :class="{ error: healthData.status !== 'ok' }"></span>
|
|
|
|
|
服务状态
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-value">{{ healthData.status === 'ok' ? '正常运行' : '配置错误' }}</div>
|
|
|
|
|
<div class="stat-label">运行时长: {{ formatUptime(healthData.uptime) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-08-15 13:30:35 +08:00
|
|
|
<div class="card-title"><span class="dot"></span>配置状态</div>
|
2026-05-20 19:48:55 +08:00
|
|
|
<div class="info-row">
|
|
|
|
|
<span class="info-label">环境变量</span>
|
|
|
|
|
<span class="info-value" :class="healthData.config_status?.all_required_vars_set ? 'success' : 'error'">
|
|
|
|
|
{{ healthData.config_status?.all_required_vars_set ? '已配置' : '未配置' }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="info-row">
|
|
|
|
|
<span class="info-label">启动时间</span>
|
|
|
|
|
<span class="info-value">{{ healthData.start_time || '-' }}</span>
|
|
|
|
|
</div>
|
2026-08-15 13:30:35 +08:00
|
|
|
<div class="info-row">
|
|
|
|
|
<span class="info-label">详细指标</span>
|
|
|
|
|
<a class="info-value" href="/metrics" target="_blank" style="color:#00d4ff">/metrics</a>
|
|
|
|
|
</div>
|
2026-05-20 19:48:55 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-08-15 13:30:35 +08:00
|
|
|
<div class="card-title"><span class="dot"></span>内存使用</div>
|
2026-05-20 19:48:55 +08:00
|
|
|
<div class="memory-stat">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="value">{{ formatBytes(healthData.memory?.heap_alloc) }}</div>
|
|
|
|
|
<div class="label">Heap Alloc</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="value">{{ formatBytes(healthData.memory?.heap_inuse) }}</div>
|
|
|
|
|
<div class="label">Heap Inuse</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="value">{{ healthData.memory?.goroutines || 0 }}</div>
|
|
|
|
|
<div class="label">Goroutines</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-08-15 13:30:35 +08:00
|
|
|
<div class="card-title"><span class="dot"></span>指标预览 (Prometheus 文本格式)</div>
|
|
|
|
|
<iframe class="metrics-frame" src="/metrics"></iframe>
|
2026-05-20 19:48:55 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-08-15 13:30:35 +08:00
|
|
|
<div v-if="loading && !healthData.status" class="loading">加载中...</div>
|
2026-05-20 19:48:55 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-08-15 13:30:35 +08:00
|
|
|
const { createApp, ref, onMounted } = Vue;
|
2026-05-20 19:48:55 +08:00
|
|
|
|
|
|
|
|
createApp({
|
|
|
|
|
setup() {
|
|
|
|
|
const healthData = ref({});
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const error = ref(null);
|
|
|
|
|
|
|
|
|
|
const deepUpdate = (target, source) => {
|
|
|
|
|
for (const key of Object.keys(source)) {
|
|
|
|
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
|
|
|
if (!target[key]) target[key] = {};
|
|
|
|
|
deepUpdate(target[key], source[key]);
|
|
|
|
|
} else {
|
|
|
|
|
target[key] = source[key];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchHealth = async (showLoading = false) => {
|
2026-08-15 13:30:35 +08:00
|
|
|
if (showLoading) loading.value = true;
|
2026-05-20 19:48:55 +08:00
|
|
|
try {
|
|
|
|
|
const response = await axios.get('/health');
|
|
|
|
|
deepUpdate(healthData.value, response.data);
|
2026-08-15 13:30:35 +08:00
|
|
|
error.value = null;
|
2026-05-20 19:48:55 +08:00
|
|
|
} catch (e) {
|
|
|
|
|
if (e.response && e.response.data) {
|
|
|
|
|
deepUpdate(healthData.value, e.response.data);
|
2026-05-22 23:32:10 +08:00
|
|
|
if (e.response.status === 503) {
|
|
|
|
|
error.value = '服务配置异常,请检查环境变量配置';
|
2026-05-20 19:48:55 +08:00
|
|
|
} else {
|
|
|
|
|
error.value = '服务异常: ' + (e.message || '未知错误');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
error.value = '无法获取服务状态: ' + (e.message || '未知错误');
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatBytes = (bytes) => {
|
|
|
|
|
if (!bytes) return '0 B';
|
|
|
|
|
const k = 1024;
|
|
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatUptime = (seconds) => {
|
|
|
|
|
if (!seconds) return '-';
|
|
|
|
|
const s = parseInt(seconds);
|
|
|
|
|
const d = Math.floor(s / 86400);
|
|
|
|
|
const h = Math.floor((s % 86400) / 3600);
|
|
|
|
|
const m = Math.floor((s % 3600) / 60);
|
|
|
|
|
if (d > 0) return `${d}天 ${h}小时`;
|
|
|
|
|
if (h > 0) return `${h}小时 ${m}分钟`;
|
|
|
|
|
return `${m}分钟`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchHealth(true);
|
|
|
|
|
setInterval(() => fetchHealth(false), 10000);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-15 13:30:35 +08:00
|
|
|
return { healthData, loading, error, fetchHealth, formatBytes, formatUptime };
|
2026-05-20 19:48:55 +08:00
|
|
|
}
|
|
|
|
|
}).mount('#app');
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
2026-08-15 13:30:35 +08:00
|
|
|
</html>
|