1009 lines
33 KiB
JavaScript

const { createApp } = Vue;
const STORAGE_KEY = "hy-app-monitor-refresh-interval";
const UPDATE_OPERATION_STORAGE_KEY = "hy-app-monitor-service-update-operation";
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: [],
},
},
},
nacosConfig: {
loading: false,
error: "",
keyword: "",
items: [],
selectedKey: "",
detailLoading: false,
detailError: "",
validating: false,
validationMessage: "",
saving: false,
saveMessage: "",
loadedContent: "",
editor: {
group: "",
dataId: "",
type: "",
appName: "",
description: "",
lastModifiedTime: "",
md5: "",
content: "",
},
},
goConfig: {
loading: false,
error: "",
validating: false,
validationMessage: "",
saving: false,
saveMessage: "",
loadedContent: "",
editorContent: "",
diffKeys: [],
hosts: [],
},
updateOps: {
loading: false,
error: "",
running: false,
operationId: "",
status: "",
stage: "",
startedAt: "",
finishedAt: "",
logDroppedCount: 0,
logs: [],
message: "",
items: [],
selectedServices: [],
javaGitRef: "main",
goGitRef: "main",
imageTag: "",
skipMavenBuild: false,
defaults: {
javaGitRef: "main",
goGitRef: "main",
dockerPlatform: "linux/amd64",
},
builder: {
harborRegistry: "",
harborProject: "",
javaRepoRoot: "",
golangRepoRoot: "",
},
result: null,
pollTimer: null,
statusRequestInFlight: false,
statusRequestSeq: 0,
statusAppliedSeq: 0,
requestEpoch: 0,
},
restartOps: {
loading: false,
error: "",
running: false,
message: "",
items: [],
selectedServices: [],
result: null,
},
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)
);
});
},
filteredNacosConfigs() {
const keyword = this.nacosConfig.keyword.trim().toLowerCase();
return (this.nacosConfig.items || []).filter((item) => {
if (!keyword) {
return true;
}
return [item.group, item.dataId, item.type, item.appName, item.description]
.join(" ")
.toLowerCase()
.includes(keyword);
});
},
nacosConfigDirty() {
return this.nacosConfig.editor.content !== this.nacosConfig.loadedContent;
},
goConfigDirty() {
return this.goConfig.editorContent !== this.goConfig.loadedContent;
},
restartTargetMap() {
return Object.fromEntries((this.restartOps.items || []).map((item) => [item.service, item]));
},
updateTargetMap() {
return Object.fromEntries((this.updateOps.items || []).map((item) => [item.service, item]));
},
updateLogText() {
return (this.updateOps.logs || [])
.map((entry) => {
const time = this.formatDateTime(entry.time);
const stage = entry.stage ? ` [${entry.stage}]` : "";
const level = entry.level && entry.level !== "info" ? ` (${entry.level})` : "";
return `${time}${stage}${level} ${entry.message || ""}`.trim();
})
.join("\n");
},
},
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"}`;
},
configKey(group, dataId) {
return `${group}@@${dataId}`;
},
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`;
},
restartTargetLabel(item) {
return `${item.service} · ${item.kind} · ${item.hosts.map((host) => host.host).join(" / ")}`;
},
updateTargetLabel(item) {
return `${item.service} · ${item.kind} · ${item.hosts.map((host) => host.host).join(" / ")}`;
},
updateLogDownloadUrl() {
if (!this.updateOps.operationId) {
return "";
}
const query = new URLSearchParams({ id: this.updateOps.operationId });
return `/api/ops/update-log?${query.toString()}`;
},
updateStatusBadgeClass() {
if (this.updateOps.error || this.updateOps.status === "failed") {
return "bad";
}
if (this.updateOps.running || this.updateOps.status === "running" || this.updateOps.status === "queued") {
return "warn";
}
if (this.updateOps.status === "success") {
return "ok";
}
return "neutral";
},
updateStatusLabel() {
if (this.updateOps.error || this.updateOps.status === "failed") {
return "失败";
}
if (this.updateOps.status === "queued") {
return "排队中";
}
if (this.updateOps.running || this.updateOps.status === "running") {
return "执行中";
}
if (this.updateOps.status === "success") {
return "已完成";
}
return "未开始";
},
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();
}
},
persistUpdateOperationId() {
if (this.updateOps.operationId) {
window.sessionStorage.setItem(UPDATE_OPERATION_STORAGE_KEY, this.updateOps.operationId);
return;
}
window.sessionStorage.removeItem(UPDATE_OPERATION_STORAGE_KEY);
},
stopUpdatePolling() {
if (this.updateOps.pollTimer) {
window.clearInterval(this.updateOps.pollTimer);
this.updateOps.pollTimer = null;
}
},
scrollUpdateLogToBottom() {
this.$nextTick(() => {
const node = document.getElementById("service-update-log-output");
if (node) {
node.scrollTop = node.scrollHeight;
}
});
},
applyUpdateOperation(operation) {
if (!operation) {
return;
}
this.updateOps.operationId = operation.operationId || "";
this.updateOps.status = operation.status || "";
this.updateOps.stage = operation.stage || "";
this.updateOps.startedAt = operation.startedAt || "";
this.updateOps.finishedAt = operation.finishedAt || "";
this.updateOps.logDroppedCount = Number(operation.logDroppedCount || 0);
this.updateOps.logs = Array.isArray(operation.logs) ? operation.logs : [];
this.updateOps.result = operation.result || null;
this.updateOps.running = operation.status === "queued" || operation.status === "running";
if (operation.status === "success") {
this.updateOps.message = operation.message || operation.result?.message || "服务更新完成";
this.updateOps.error = "";
} else if (operation.status === "failed") {
this.updateOps.error = operation.error || operation.result?.error || "服务更新失败";
this.updateOps.message = operation.message || "";
} else {
this.updateOps.message = operation.message || "";
}
this.persistUpdateOperationId();
this.scrollUpdateLogToBottom();
},
startUpdatePolling() {
this.stopUpdatePolling();
if (!this.updateOps.operationId) {
return;
}
this.updateOps.pollTimer = window.setInterval(() => {
this.fetchUpdateOperationStatus(this.updateOps.operationId, { silent: true });
}, 2000);
},
async fetchUpdateOperationStatus(operationId, { silent = false } = {}) {
if (!operationId) {
return;
}
// 轮询只保留一个在途请求,避免旧响应晚到后把新日志覆盖掉。
if (this.updateOps.statusRequestInFlight) {
return;
}
const wasRunning = this.updateOps.running;
const requestSeq = this.updateOps.statusRequestSeq + 1;
const requestEpoch = this.updateOps.requestEpoch;
this.updateOps.statusRequestSeq = requestSeq;
this.updateOps.statusRequestInFlight = true;
try {
const query = new URLSearchParams({ id: operationId });
const response = await fetch(`/api/ops/update-status?${query.toString()}`, { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
// 新任务已经开始时,直接丢掉旧任务轮询结果。
if (requestEpoch !== this.updateOps.requestEpoch) {
return;
}
if (this.updateOps.operationId && this.updateOps.operationId !== operationId) {
return;
}
// 保护日志顺序,旧轮询结果不能回退页面状态。
if (requestSeq < this.updateOps.statusAppliedSeq) {
return;
}
this.updateOps.statusAppliedSeq = requestSeq;
this.applyUpdateOperation(payload.operation || {});
if (!this.updateOps.running) {
this.stopUpdatePolling();
if (wasRunning) {
await this.refresh();
await this.refreshUpdateTargets();
await this.refreshRestartTargets();
}
}
} catch (error) {
if (requestEpoch !== this.updateOps.requestEpoch) {
return;
}
if (this.updateOps.operationId && this.updateOps.operationId !== operationId) {
return;
}
const errorMessage = error instanceof Error ? error.message : String(error);
const notFound = errorMessage.includes("update operation not found") || errorMessage.includes("HTTP 404");
if (silent && !notFound) {
return;
}
this.stopUpdatePolling();
this.updateOps.running = false;
if (notFound) {
this.updateOps.status = "failed";
this.updateOps.error = "更新任务状态不存在,可能因为服务重启或旧任务已清理";
this.updateOps.message = "";
this.updateOps.operationId = "";
this.persistUpdateOperationId();
return;
}
if (this.updateOps.status === "queued" || this.updateOps.status === "running") {
this.updateOps.status = "failed";
this.updateOps.message = "更新状态查询失败";
}
this.updateOps.error = errorMessage;
} finally {
if (requestEpoch === this.updateOps.requestEpoch && this.updateOps.statusRequestSeq === requestSeq) {
this.updateOps.statusRequestInFlight = false;
}
}
},
applyRefreshTimer() {
if (this.timer) {
window.clearInterval(this.timer);
}
this.timer = window.setInterval(() => this.refresh(), this.refreshIntervalSeconds * 1000);
},
resetNacosEditor() {
this.nacosConfig.selectedKey = "";
this.nacosConfig.detailError = "";
this.nacosConfig.validationMessage = "";
this.nacosConfig.saveMessage = "";
this.nacosConfig.loadedContent = "";
this.nacosConfig.editor = {
group: "",
dataId: "",
type: "",
appName: "",
description: "",
lastModifiedTime: "",
md5: "",
content: "",
};
},
async readJsonResponse(response) {
const text = await response.text();
if (!text) {
return {};
}
try {
return JSON.parse(text);
} catch (error) {
throw new Error(text);
}
},
async refresh() {
this.loading = true;
this.error = "";
try {
const response = await fetch("/api/monitor/services", { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.payload = payload;
this.syncRefreshSettings();
} catch (error) {
this.error = error instanceof Error ? error.message : String(error);
} finally {
this.loading = false;
}
},
async refreshNacosConfigs({ preserveSelection = true, reloadSelected = false } = {}) {
const previousKey = preserveSelection ? this.nacosConfig.selectedKey : "";
this.nacosConfig.loading = true;
this.nacosConfig.error = "";
this.nacosConfig.saveMessage = "";
try {
const response = await fetch("/api/nacos/configs", { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.nacosConfig.items = Array.isArray(payload.items) ? payload.items : [];
const hasPrevious = previousKey
&& this.nacosConfig.items.some((item) => this.configKey(item.group, item.dataId) === previousKey);
const nextKey = hasPrevious
? previousKey
: (this.nacosConfig.items[0]
? this.configKey(this.nacosConfig.items[0].group, this.nacosConfig.items[0].dataId)
: "");
if (!nextKey) {
this.resetNacosEditor();
return;
}
this.nacosConfig.selectedKey = nextKey;
const sameAsCurrent = this.configKey(this.nacosConfig.editor.group, this.nacosConfig.editor.dataId) === nextKey;
if (!sameAsCurrent || reloadSelected || !this.nacosConfig.loadedContent) {
await this.openNacosConfigByKey(nextKey);
}
} catch (error) {
this.nacosConfig.error = error instanceof Error ? error.message : String(error);
} finally {
this.nacosConfig.loading = false;
}
},
async openNacosConfig(item) {
const nextKey = this.configKey(item.group, item.dataId);
this.nacosConfig.selectedKey = nextKey;
await this.openNacosConfigByKey(nextKey);
},
async openNacosConfigByKey(configKey) {
const item = (this.nacosConfig.items || []).find((entry) => this.configKey(entry.group, entry.dataId) === configKey);
if (!item) {
return;
}
this.nacosConfig.detailLoading = true;
this.nacosConfig.detailError = "";
this.nacosConfig.validationMessage = "";
this.nacosConfig.saveMessage = "";
try {
const query = new URLSearchParams({ group: item.group, dataId: item.dataId });
const response = await fetch(`/api/nacos/config?${query.toString()}`, { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.nacosConfig.loadedContent = typeof payload.content === "string" ? payload.content : "";
this.nacosConfig.editor = {
group: item.group,
dataId: item.dataId,
type: item.type || payload.item?.type || "",
appName: item.appName || "",
description: item.description || "",
lastModifiedTime: item.lastModifiedTime || "",
md5: item.md5 || "",
content: this.nacosConfig.loadedContent,
};
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
} finally {
this.nacosConfig.detailLoading = false;
}
},
async validateNacosConfig() {
if (!this.nacosConfig.editor.dataId) {
return;
}
this.nacosConfig.validating = true;
this.nacosConfig.detailError = "";
this.nacosConfig.validationMessage = "";
try {
const response = await fetch("/api/nacos/config/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
group: this.nacosConfig.editor.group,
dataId: this.nacosConfig.editor.dataId,
type: this.nacosConfig.editor.type,
content: this.nacosConfig.editor.content,
}),
});
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
if (!payload.ok) {
throw new Error(payload.message || "config validation failed");
}
this.nacosConfig.validationMessage = payload.message || "格式校验通过";
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
} finally {
this.nacosConfig.validating = false;
}
},
async saveNacosConfig() {
if (!this.nacosConfig.editor.group || !this.nacosConfig.editor.dataId) {
return;
}
this.nacosConfig.saving = true;
this.nacosConfig.detailError = "";
this.nacosConfig.saveMessage = "";
try {
const response = await fetch("/api/nacos/config", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
group: this.nacosConfig.editor.group,
dataId: this.nacosConfig.editor.dataId,
type: this.nacosConfig.editor.type,
content: this.nacosConfig.editor.content,
}),
});
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.nacosConfig.loadedContent = this.nacosConfig.editor.content;
this.nacosConfig.saveMessage = `已保存 ${this.nacosConfig.editor.group}/${this.nacosConfig.editor.dataId}`;
await this.refreshNacosConfigs({ preserveSelection: true, reloadSelected: true });
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
} finally {
this.nacosConfig.saving = false;
}
},
async refreshGoConfig() {
this.goConfig.loading = true;
this.goConfig.error = "";
this.goConfig.saveMessage = "";
try {
const response = await fetch("/api/runtime/golang-config", { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.goConfig.hosts = Array.isArray(payload.hosts) ? payload.hosts : [];
this.goConfig.diffKeys = Array.isArray(payload.diffKeys) ? payload.diffKeys : [];
this.goConfig.loadedContent = typeof payload.editorContent === "string" ? payload.editorContent : "";
this.goConfig.editorContent = this.goConfig.loadedContent;
} catch (error) {
this.goConfig.error = error instanceof Error ? error.message : String(error);
} finally {
this.goConfig.loading = false;
}
},
async validateGoConfig() {
this.goConfig.validating = true;
this.goConfig.error = "";
this.goConfig.validationMessage = "";
try {
const response = await fetch("/api/runtime/golang-config/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ content: this.goConfig.editorContent }),
});
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
if (!payload.ok) {
throw new Error(payload.message || "golang config validation failed");
}
this.goConfig.validationMessage = payload.message || "格式校验通过";
} catch (error) {
this.goConfig.error = error instanceof Error ? error.message : String(error);
} finally {
this.goConfig.validating = false;
}
},
async saveGoConfig() {
this.goConfig.saving = true;
this.goConfig.error = "";
this.goConfig.saveMessage = "";
try {
const response = await fetch("/api/runtime/golang-config", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ content: this.goConfig.editorContent }),
});
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.goConfig.loadedContent = this.goConfig.editorContent;
this.goConfig.saveMessage = payload.message || "golang 配置已保存";
await this.refreshGoConfig();
} catch (error) {
this.goConfig.error = error instanceof Error ? error.message : String(error);
} finally {
this.goConfig.saving = false;
}
},
async refreshRestartTargets() {
this.restartOps.loading = true;
this.restartOps.error = "";
try {
const response = await fetch("/api/ops/restart-targets", { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.restartOps.items = Array.isArray(payload.items) ? payload.items : [];
const available = new Set(this.restartOps.items.map((item) => item.service));
this.restartOps.selectedServices = this.restartOps.selectedServices.filter((service) => available.has(service));
} catch (error) {
this.restartOps.error = error instanceof Error ? error.message : String(error);
} finally {
this.restartOps.loading = false;
}
},
async refreshUpdateTargets() {
this.updateOps.loading = true;
this.updateOps.error = "";
try {
const response = await fetch("/api/ops/update-targets", { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.updateOps.items = Array.isArray(payload.items) ? payload.items : [];
this.updateOps.defaults = payload.defaults || this.updateOps.defaults;
this.updateOps.builder = payload.builder || this.updateOps.builder;
if (!this.updateOps.javaGitRef) {
this.updateOps.javaGitRef = this.updateOps.defaults.javaGitRef || "main";
} else if (this.updateOps.javaGitRef === "main" && this.updateOps.defaults.javaGitRef) {
this.updateOps.javaGitRef = this.updateOps.defaults.javaGitRef;
}
if (!this.updateOps.goGitRef) {
this.updateOps.goGitRef = this.updateOps.defaults.goGitRef || "main";
} else if (this.updateOps.goGitRef === "main" && this.updateOps.defaults.goGitRef) {
this.updateOps.goGitRef = this.updateOps.defaults.goGitRef;
}
const available = new Set(this.updateOps.items.map((item) => item.service));
this.updateOps.selectedServices = this.updateOps.selectedServices.filter((service) => available.has(service));
} catch (error) {
this.updateOps.error = error instanceof Error ? error.message : String(error);
} finally {
this.updateOps.loading = false;
}
},
toggleUpdateService(serviceName) {
const next = new Set(this.updateOps.selectedServices);
if (next.has(serviceName)) {
next.delete(serviceName);
} else {
next.add(serviceName);
}
this.updateOps.selectedServices = [...next];
},
async runServiceUpdate() {
if (this.updateOps.selectedServices.length === 0) {
return;
}
this.stopUpdatePolling();
this.updateOps.requestEpoch += 1;
this.updateOps.statusRequestInFlight = false;
this.updateOps.statusAppliedSeq = 0;
this.updateOps.running = true;
this.updateOps.error = "";
this.updateOps.message = "已提交更新任务,等待开始执行";
this.updateOps.operationId = "";
this.updateOps.status = "queued";
this.updateOps.stage = "queued";
this.updateOps.startedAt = "";
this.updateOps.finishedAt = "";
this.updateOps.logDroppedCount = 0;
this.updateOps.logs = [];
this.updateOps.result = null;
this.persistUpdateOperationId();
try {
const response = await fetch("/api/ops/update-services", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
services: this.updateOps.selectedServices,
javaGitRef: this.updateOps.javaGitRef,
goGitRef: this.updateOps.goGitRef,
imageTag: this.updateOps.imageTag,
skipMavenBuild: this.updateOps.skipMavenBuild,
}),
});
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.applyUpdateOperation(payload.operation || {});
this.startUpdatePolling();
await this.fetchUpdateOperationStatus(this.updateOps.operationId, { silent: true });
} catch (error) {
this.updateOps.error = error instanceof Error ? error.message : String(error);
this.updateOps.running = false;
} finally {
this.scrollUpdateLogToBottom();
}
},
toggleRestartService(serviceName) {
const next = new Set(this.restartOps.selectedServices);
if (next.has(serviceName)) {
next.delete(serviceName);
} else {
next.add(serviceName);
}
this.restartOps.selectedServices = [...next];
},
async runRollingRestart() {
if (this.restartOps.selectedServices.length === 0) {
return;
}
this.restartOps.running = true;
this.restartOps.error = "";
this.restartOps.message = "";
try {
const response = await fetch("/api/ops/rolling-restart", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ services: this.restartOps.selectedServices }),
});
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.restartOps.result = payload;
this.restartOps.message = payload.ok
? (payload.message || "滚动重启完成")
: (payload.error || "滚动重启失败");
await this.refresh();
await this.refreshRestartTargets();
} catch (error) {
this.restartOps.error = error instanceof Error ? error.message : String(error);
} finally {
this.restartOps.running = 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();
this.refreshNacosConfigs({ preserveSelection: false, reloadSelected: true });
this.refreshGoConfig();
this.refreshUpdateTargets();
this.refreshRestartTargets();
const lastOperationId = window.sessionStorage.getItem(UPDATE_OPERATION_STORAGE_KEY) || "";
if (lastOperationId) {
this.fetchUpdateOperationStatus(lastOperationId, { silent: true });
this.startUpdatePolling();
}
},
beforeUnmount() {
if (this.timer) {
window.clearInterval(this.timer);
}
this.stopUpdatePolling();
},
}).mount("#app");