const { createApp } = Vue; const STORAGE_KEY = "hy-app-monitor-refresh-interval"; createApp({ data() { return { loading: false, error: "", filter: "all", keyword: "", refreshIntervalSeconds: 10, refreshIntervalOptions: [5, 10, 15, 30, 60], payload: { updatedAt: "", settings: { refreshIntervalSeconds: 10, refreshIntervalOptions: [5, 10, 15, 30, 60], thresholds: {}, }, summary: { total: 0, ok: 0, down: 0, hostTotal: 0, hostMetricOk: 0, hostMetricDown: 0, nacosServiceTotal: 0, nacosInstanceTotal: 0, nacosUnhealthyInstanceTotal: 0, mongoDatabaseTotal: 0, mongoCollectionTotal: 0, mongoOk: 0, }, hosts: [], nacos: { ok: false, summary: {}, services: [], namespaces: [], }, mongo: { ok: false, summary: {}, databases: [], replicaSet: { configured: false, members: [], }, }, }, timer: null, }; }, computed: { summary() { return this.payload.summary || {}; }, thresholds() { return this.payload.settings?.thresholds || {}; }, formattedUpdatedAt() { return this.formatDateTime(this.payload.updatedAt); }, groupedHosts() { const keyword = this.keyword.trim().toLowerCase(); return (this.payload.hosts || []) .map((host) => { const hostMatched = [host.host, host.ip, host.group, host.instanceId] .join(" ") .toLowerCase() .includes(keyword); const filteredItems = (host.items || []).filter((item) => { if (this.filter === "down" && item.ok) { return false; } if (this.filter === "java" && item.kind !== "java") { return false; } if (this.filter === "golang" && item.kind !== "golang") { return false; } if (!keyword) { return true; } return [item.host, item.ip, item.service, item.kind, item.group, item.url, item.detail] .join(" ") .toLowerCase() .includes(keyword); }); const shouldShowBecauseDown = this.filter === "down" && !(host.metrics && host.metrics.ok); const shouldShowBecauseKeyword = keyword ? hostMatched || filteredItems.length > 0 : true; const shouldShow = shouldShowBecauseKeyword && (this.filter === "all" || filteredItems.length > 0 || shouldShowBecauseDown || hostMatched); return { ...host, groupLabel: this.groupLabel(host.group), items: filteredItems, downCount: (host.items || []).filter((item) => !item.ok).length, shouldShow, }; }) .filter((host) => host.shouldShow); }, filteredNacosServices() { const keyword = this.keyword.trim().toLowerCase(); return (this.payload.nacos?.services || []).filter((service) => { if (!keyword) { return true; } const serviceMatched = [service.serviceName, service.groupName, service.rawServiceName] .join(" ") .toLowerCase() .includes(keyword); if (serviceMatched) { return true; } return (service.instances || []).some((instance) => [instance.ip, instance.port, instance.clusterName] .join(" ") .toLowerCase() .includes(keyword) ); }); }, }, watch: { refreshIntervalSeconds() { window.localStorage.setItem(STORAGE_KEY, String(this.refreshIntervalSeconds)); this.applyRefreshTimer(); }, }, methods: { groupLabel(group) { return { app: "应用节点", pay: "支付节点", gateway: "网关节点", nacos: "Nacos 节点", mongo: "MongoDB 节点", }[group] || group; }, toneClass(level) { return `tone-${level || "neutral"}`; }, boolTone(ok) { return ok ? "tone-ok" : "tone-bad"; }, formatPercent(value) { if (value == null || Number.isNaN(Number(value))) { return "-"; } return `${Number(value).toFixed(2)}%`; }, formatGb(used, total) { if (used == null || total == null) { return "-"; } return `${Number(used).toFixed(2)} / ${Number(total).toFixed(2)} GB`; }, formatMb(value) { if (value == null || Number.isNaN(Number(value))) { return "-"; } return `${Number(value).toFixed(2)} MB`; }, formatBytes(value) { if (value == null || Number.isNaN(Number(value))) { return "-"; } const units = ["B", "KB", "MB", "GB", "TB"]; let amount = Number(value); let index = 0; while (amount >= 1024 && index < units.length - 1) { amount /= 1024; index += 1; } return `${amount.toFixed(2)} ${units[index]}`; }, formatNumber(value) { if (value == null || Number.isNaN(Number(value))) { return "-"; } return Number(value).toLocaleString(); }, formatDateTime(value) { if (!value) { return "-"; } const date = new Date(value); return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString(); }, formatDuration(seconds) { if (seconds == null || Number.isNaN(Number(seconds))) { return "-"; } const total = Math.max(0, Number(seconds)); const days = Math.floor(total / 86400); const hours = Math.floor((total % 86400) / 3600); const minutes = Math.floor((total % 3600) / 60); const remainSeconds = Math.floor(total % 60); if (days > 0) { return `${days}d ${hours}h ${minutes}m`; } if (hours > 0) { return `${hours}h ${minutes}m ${remainSeconds}s`; } if (minutes > 0) { return `${minutes}m ${remainSeconds}s`; } return `${remainSeconds}s`; }, syncRefreshSettings() { const settings = this.payload.settings || {}; const options = Array.isArray(settings.refreshIntervalOptions) ? settings.refreshIntervalOptions.map((item) => Number(item)).filter((item) => !Number.isNaN(item) && item > 0) : this.refreshIntervalOptions; if (options.length > 0) { this.refreshIntervalOptions = options; } const stored = Number(window.localStorage.getItem(STORAGE_KEY) || 0); const configured = Number(settings.refreshIntervalSeconds || 0); const fallback = this.refreshIntervalOptions[0] || 10; const candidate = this.refreshIntervalOptions.includes(stored) ? stored : this.refreshIntervalOptions.includes(configured) ? configured : fallback; if (candidate !== this.refreshIntervalSeconds) { this.refreshIntervalSeconds = candidate; } else { this.applyRefreshTimer(); } }, applyRefreshTimer() { if (this.timer) { window.clearInterval(this.timer); } this.timer = window.setInterval(() => this.refresh(), this.refreshIntervalSeconds * 1000); }, async refresh() { this.loading = true; this.error = ""; try { const response = await fetch("/api/monitor/services", { cache: "no-store" }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } this.payload = await response.json(); this.syncRefreshSettings(); } catch (error) { this.error = error instanceof Error ? error.message : String(error); } finally { this.loading = false; } }, }, mounted() { const root = document.getElementById("app"); const bootIndicator = document.getElementById("boot-indicator"); if (root) { root.removeAttribute("v-cloak"); } if (bootIndicator) { bootIndicator.hidden = true; } this.refresh(); }, beforeUnmount() { if (this.timer) { window.clearInterval(this.timer); } }, }).mount("#app");