From 579e8a4aa5af09d41f68adef0c3a768cf526eecc Mon Sep 17 00:00:00 2001 From: hy Date: Thu, 23 Apr 2026 12:30:03 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B2=BE=E7=AE=80yumi=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E9=A1=B5=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- monitors/yumi/dashboard.py | 11 +- monitors/yumi/service_release.py | 76 ++++++---- static/app.css | 229 ++++++++++++++++--------------- static/app.js | 72 +++++----- static/index.html | 133 +++++++----------- tests/test_service_release.py | 95 +++++++++++++ 6 files changed, 356 insertions(+), 260 deletions(-) create mode 100644 tests/test_service_release.py diff --git a/monitors/yumi/dashboard.py b/monitors/yumi/dashboard.py index d3387af..3e7d109 100644 --- a/monitors/yumi/dashboard.py +++ b/monitors/yumi/dashboard.py @@ -6,7 +6,6 @@ from core.config import THRESHOLDS, load_hosts, settings_payload, utc_now from .host_metrics import get_host_metrics from .http_probe import get_service_payload from .mongo_metrics import get_mongo_payload -from .mysql_metrics import get_mysql_overview_payload from .nacos import get_nacos_payload @@ -117,8 +116,6 @@ def build_monitor_payload() -> dict[str, Any]: nacos_payload = get_nacos_payload() # 获取 Mongo 指标。 mongo_payload = get_mongo_payload() - # 获取 MySQL 指标。 - mysql_payload = get_mysql_overview_payload() # 组装主机维度视图。 grouped_hosts = build_host_payload(hosts, service_payload, metrics_by_host) # 统计主机指标成功数。 @@ -140,13 +137,13 @@ def build_monitor_payload() -> dict[str, Any]: "mongoDatabaseTotal": int(mongo_payload.get("summary", {}).get("databaseTotal", 0)), "mongoCollectionTotal": int(mongo_payload.get("summary", {}).get("collectionTotal", 0)), "mongoOk": 1 if mongo_payload.get("ok") else 0, - "mysqlDatabaseTotal": int(mysql_payload.get("summary", {}).get("databaseTotal", 0)), - "mysqlTableTotal": int(mysql_payload.get("summary", {}).get("tableTotal", 0)), - "mysqlOk": 1 if mysql_payload.get("ok") else 0, + "mysqlDatabaseTotal": 0, + "mysqlTableTotal": 0, + "mysqlOk": 0, }, "hosts": grouped_hosts, "items": service_payload["items"], "nacos": nacos_payload, "mongo": mongo_payload, - "mysql": mysql_payload, + "mysql": {}, } diff --git a/monitors/yumi/service_release.py b/monitors/yumi/service_release.py index 365a60a..11c0351 100644 --- a/monitors/yumi/service_release.py +++ b/monitors/yumi/service_release.py @@ -1402,42 +1402,70 @@ def build_update_targets_payload() -> dict[str, Any]: # 页面需要展示当前可更新服务、宿主机,以及本地构建所依赖的仓路径。 records_map = service_records_by_name() items: list[dict[str, Any]] = [] + # 同一台主机会承载多个服务,这里先把 compose 模型按主机缓存起来,避免重复读模板。 + compose_models_by_host: dict[str, dict[str, Any] | None] = {} + # 模板读取失败不能拖垮整页,这里保留每台主机的错误给后续条目复用。 + compose_errors_by_host: dict[str, str] = {} + + def host_compose_model(host_name: str) -> dict[str, Any] | None: + # 每台主机只解析一次本地 compose;失败时返回空并记下错误摘要。 + if host_name in compose_models_by_host: + return compose_models_by_host[host_name] + if host_name in compose_errors_by_host: + return None + + try: + compose_models_by_host[host_name] = load_local_host_compose_model(host_name) + return compose_models_by_host[host_name] + except Exception as exc: # noqa: BLE001 + compose_errors_by_host[host_name] = short_text(str(exc), 200) + compose_models_by_host[host_name] = None + return None for service_name in buildable_service_names(): records = records_map.get(service_name) or [] if not records: continue current_images: list[str] = [] + compose_warnings: list[str] = [] for record in records: - model = load_local_host_compose_model(record["host"]) + model = host_compose_model(record["host"]) + if model is None: + compose_error = compose_errors_by_host.get(record["host"], "compose template unavailable") + warning_text = f"{record['host']}: {compose_error}" + if warning_text not in compose_warnings: + compose_warnings.append(warning_text) + continue image_ref = str(((model.get("services") or {}).get(service_name) or {}).get("image") or "").strip() if image_ref and image_ref not in current_images: current_images.append(image_ref) display = image_release_display(current_images) - items.append( - { - "service": service_name, - "kind": service_kind(service_name), - "repository": service_repository_ref(service_name), - "currentLabel": display["currentLabel"], - "currentTitle": display["currentTitle"], - "detailLabel": display["detailLabel"], - "detailTitle": display["detailTitle"], - "copyValue": display["copyValue"], - "currentImages": current_images, - "hosts": [ - { - "host": record["host"], - "ip": record["ip"], - "instanceId": record["instanceId"], - "port": record["port"], - "path": record["path"], - } - for record in records - ], - } - ) + item = { + "service": service_name, + "kind": service_kind(service_name), + "repository": service_repository_ref(service_name), + "currentLabel": display["currentLabel"], + "currentTitle": display["currentTitle"], + "detailLabel": display["detailLabel"], + "detailTitle": display["detailTitle"], + "copyValue": display["copyValue"], + "currentImages": current_images, + "hosts": [ + { + "host": record["host"], + "ip": record["ip"], + "instanceId": record["instanceId"], + "port": record["port"], + "path": record["path"], + } + for record in records + ], + } + # 某些主机模板暂时不可读时,仍然保留服务条目,只把原因透出去。 + if compose_warnings: + item["warnings"] = compose_warnings + items.append(item) if frontend_release_enabled(): frontend_service = ADMIN_FRONTEND_SERVICE_NAME diff --git a/static/app.css b/static/app.css index b1674dc..d170db4 100644 --- a/static/app.css +++ b/static/app.css @@ -2720,7 +2720,9 @@ body.auth-page { .release-status-strip { margin-top: 8px; - padding: 10px 12px; + padding: 12px; + background: var(--panel); + box-shadow: none; } .sticky-release-strip { @@ -2730,17 +2732,12 @@ body.auth-page { } .release-action-bar { - position: relative; display: flex; justify-content: space-between; gap: 12px; - align-items: center; - margin-top: 10px; - padding: 10px 12px; - border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong)); - border-radius: 14px; - background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%); - box-shadow: var(--shadow-soft), inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent); + align-items: flex-start; + padding-bottom: 10px; + border-bottom: 1px solid var(--line); } .release-action-summary { @@ -2748,38 +2745,34 @@ body.auth-page { gap: 8px; flex-wrap: wrap; align-items: center; - padding: 4px; - border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong)); - border-radius: 12px; - background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--panel-muted)) 0%, var(--panel-strong) 100%); - box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent); } .release-status-grid { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; + margin-top: 10px; } .release-status-item { position: relative; - overflow: hidden; - min-height: 56px; + min-height: 0; padding: 8px 10px; border: 1px solid var(--line); - border-radius: 12px; - background: var(--panel-muted); - box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 68%, transparent); + border-radius: 10px; + background: var(--panel-strong); + box-shadow: none; } .release-status-item::before { content: ""; position: absolute; left: 0; - right: 0; - top: 0; - height: 2px; - background: linear-gradient(90deg, color-mix(in oklab, currentColor 46%, transparent) 0%, transparent 78%); + top: 8px; + bottom: 8px; + width: 3px; + border-radius: 999px; + background: color-mix(in oklab, currentColor 46%, transparent); opacity: 0.82; } @@ -2819,46 +2812,47 @@ body.auth-page { .release-card-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(228px, 1fr)); - gap: 12px; - margin-top: 12px; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 10px; + margin-top: 10px; } .release-service-card { --release-kind-color: var(--accent); position: relative; display: grid; - gap: 8px; - padding: 11px 11px 10px; + gap: 10px; + padding: 10px 12px; border: 1px solid var(--line); - border-radius: 15px; - background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%); - transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, box-shadow 180ms ease; + border-radius: 12px; + background: var(--panel-strong); + transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease; + box-shadow: none; } .release-service-card::before { content: ""; position: absolute; left: 0; - right: 0; - top: 0; - height: 3px; - background: linear-gradient(90deg, var(--release-kind-color) 0%, transparent 78%); + top: 8px; + bottom: 8px; + width: 3px; + border-radius: 999px; + background: var(--release-kind-color); } .release-service-card:hover { - transform: translateY(-1px); border-color: color-mix(in oklab, var(--accent) 28%, var(--line)); } .release-service-card.selected { border-color: color-mix(in oklab, var(--accent) 38%, var(--line)); - background: linear-gradient(180deg, color-mix(in oklab, var(--accent-soft) 38%, var(--panel-strong)) 0%, color-mix(in oklab, var(--accent-soft) 20%, var(--panel-muted)) 100%); + background: color-mix(in oklab, var(--accent-soft) 26%, var(--panel-strong)); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent); } .release-service-card.selected::before { - background: linear-gradient(90deg, color-mix(in oklab, var(--accent) 78%, var(--release-kind-color)) 0%, transparent 84%); + background: color-mix(in oklab, var(--accent) 78%, var(--release-kind-color)); } .release-service-card.selected .release-service-title strong { @@ -2880,17 +2874,22 @@ body.auth-page { .release-service-head { display: flex; justify-content: space-between; - gap: 10px; + gap: 12px; align-items: flex-start; } .release-service-title { + display: grid; + gap: 6px; + flex: 1 1 auto; min-width: 0; } -.release-service-title strong, -.release-service-title small { - display: block; +.release-service-title-line { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; } .release-service-title strong { @@ -2898,46 +2897,46 @@ body.auth-page { line-height: 1.3; } -.release-service-tags { - display: flex; - gap: 8px; - flex-wrap: wrap; - align-items: center; -} - -.release-service-tags .badge { - min-height: 24px; +.release-service-title-line .badge { + min-height: 22px; padding: 0 8px; font-size: 10px; } -.release-service-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; -} - -.release-service-metric { - display: grid; - gap: 4px; - min-width: 0; - padding: 8px 9px; - border: 1px solid var(--line); - border-radius: 12px; - background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--accent-soft)) 0%, var(--panel-strong) 100%); -} - -.release-service-metric strong, -.release-service-metric span { - display: block; - min-width: 0; +.release-service-meta { + display: flex; + flex-wrap: wrap; + gap: 6px 12px; + color: var(--muted); font-size: 11px; - line-height: 1.4; + line-height: 1.45; +} + +.release-service-meta span { + min-width: 0; word-break: break-word; } -.release-service-metric-wide { - grid-column: 1 / -1; +.release-service-actions { + display: grid; + gap: 8px; + justify-items: end; + align-self: center; +} + +.release-service-actions-tight { + align-self: flex-start; +} + +.release-service-note { + color: var(--muted); + margin: 0; +} + +.release-service-actions .inline-copy { + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; } .precheck-strip { @@ -2946,16 +2945,16 @@ body.auth-page { flex-wrap: wrap; align-items: center; margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--line); } .release-log-preview { display: grid; - gap: 10px; + gap: 8px; margin-top: 12px; - padding: 12px; - border: 1px solid var(--line); - border-radius: 16px; - background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%); + padding-top: 10px; + border-top: 1px solid var(--line); } .release-log-preview-head { @@ -2969,55 +2968,50 @@ body.auth-page { font-size: 12px; } -.release-log-snippets { +.release-log-list { display: grid; - gap: 8px; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 6px; } -.release-log-snippet { +.release-log-entry { display: grid; - gap: 4px; - min-height: 86px; - padding: 10px 11px; + grid-template-columns: minmax(112px, 132px) 72px minmax(0, 1fr); + gap: 10px; + align-items: start; + padding: 8px 10px; border: 1px solid var(--line); - border-radius: 12px; - background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, var(--panel-strong) 100%); - transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; + border-radius: 10px; + background: var(--panel-strong); } -.release-log-snippet:hover { - transform: translateY(-1px); - border-color: color-mix(in oklab, var(--accent) 20%, var(--line)); -} - -.release-log-snippet strong, -.release-log-snippet span, -.release-log-snippet p { +.release-log-entry strong, +.release-log-entry span, +.release-log-entry small { margin: 0; + min-width: 0; } -.release-log-snippet strong { +.release-log-entry strong { font-size: 11px; } -.release-log-snippet span { +.release-log-entry span { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; } -.release-log-snippet p { +.release-log-entry small { color: var(--text); font-size: 11px; line-height: 1.45; word-break: break-word; } -.release-log-snippet.tone-bad { +.release-log-entry.tone-bad { border-color: color-mix(in oklab, var(--bad) 28%, var(--line)); - background: color-mix(in oklab, var(--bad-soft) 44%, var(--panel-strong)); + background: color-mix(in oklab, var(--bad-soft) 28%, var(--panel-strong)); } .release-table td strong { @@ -3442,15 +3436,14 @@ body.auth-page { } .task-page-head { - margin-top: 12px; - padding: 12px 14px; - background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, color-mix(in oklab, var(--panel) 98%, var(--panel-muted)) 100%); - box-shadow: var(--shadow-soft), inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent); + margin-top: 8px; + padding: 10px 12px; + background: var(--panel); + box-shadow: none; } .task-page-head::before, .config-workbench-panel::before, -.release-action-bar::before, .editor-toolbar::before { content: ""; position: absolute; @@ -3473,10 +3466,10 @@ body.auth-page { } .task-page-head .panel-title p { - margin-top: 5px; - font-size: 10px; - letter-spacing: 0.1em; - text-transform: uppercase; + margin-top: 4px; + font-size: 11px; + letter-spacing: 0.04em; + text-transform: none; } .task-page-head .panel-meta .badge.neutral { @@ -4496,6 +4489,18 @@ body.auth-page { align-items: stretch; } + .release-service-actions { + justify-items: start; + } + + .release-service-actions .inline-copy { + justify-content: flex-start; + } + + .release-log-entry { + grid-template-columns: 1fr; + } + .diff-row { grid-template-columns: 34px 34px 22px minmax(0, 1fr); gap: 8px; diff --git a/static/app.js b/static/app.js index d669f4b..4822b1e 100644 --- a/static/app.js +++ b/static/app.js @@ -359,14 +359,14 @@ createApp({ data() { return { activeView: CURRENT_APP_CONTEXT.mode === "mysql" ? "database" : "overview", - databaseEngine: "mysql", + databaseEngine: CURRENT_APP_CONTEXT.mode === "mysql" ? "mysql" : "mongo", densityMode: "compact", detailPanelOpen: false, overviewHealthyHostsOpen: false, viewTabs: CURRENT_APP_CONTEXT.mode === "mysql" ? MYSQL_MODULE_VIEW_TABS : VIEW_TABS, databaseEngines: CURRENT_APP_CONTEXT.mode === "mysql" ? DATABASE_ENGINES.filter((engine) => engine.id === "mysql") - : DATABASE_ENGINES, + : DATABASE_ENGINES.filter((engine) => engine.id === "mongo"), primaryFilters: PRIMARY_FILTERS, releaseSections: RELEASE_SECTIONS, configTabs: CONFIG_TABS, @@ -508,6 +508,7 @@ createApp({ mySqlAdmin: createMySqlAdminState(), updateOps: { loading: false, + loaded: false, error: "", running: false, operationId: "", @@ -549,6 +550,7 @@ createApp({ }, restartOps: { loading: false, + loaded: false, error: "", running: false, message: "", @@ -1574,7 +1576,6 @@ createApp({ const hostAlertTotal = this.allHostRows.filter((host) => host.hasAlert).length; const nacosUnavailable = !(this.payload.nacos && this.payload.nacos.ok); const mongoUnavailable = !(this.payload.mongo && this.payload.mongo.ok); - const mysqlUnavailable = !(this.payload.mysql && this.payload.mysql.ok); const tatOk = this.summary.hostMetricOk || 0; const tatTotal = this.summary.hostTotal || 0; @@ -1619,14 +1620,6 @@ createApp({ tone: mongoUnavailable ? "bad" : "ok", visible: mongoUnavailable, }, - { - key: "mysql", - label: "MySQL", - value: mysqlUnavailable ? "ERR" : String(this.summary.mysqlDatabaseTotal || 0), - suffix: mysqlUnavailable ? "" : "库", - tone: mysqlUnavailable ? "bad" : "ok", - visible: mysqlUnavailable, - }, { key: "tat", label: "TAT", @@ -1666,15 +1659,6 @@ createApp({ tone: "ok", }); } - if (this.payload.mysql && this.payload.mysql.ok) { - cards.push({ - key: "mysql", - label: "MySQL", - value: `${this.summary.mysqlDatabaseTotal || 0} 库`, - meta: "连接正常", - tone: "ok", - }); - } if ((this.summary.hostMetricDown || 0) === 0 && (this.summary.hostTotal || 0) > 0) { cards.push({ key: "tat", @@ -1868,17 +1852,6 @@ createApp({ }); } - if (this.payload.mysql.error || !(this.payload.mysql && this.payload.mysql.ok)) { - alerts.push({ - key: "mysql-error", - type: "database", - view: "database", - tone: "bad", - title: "MySQL", - text: this.payload.mysql.error || "连接异常", - }); - } - return alerts.slice(0, 5); }, restartTargetMap() { @@ -2001,6 +1974,9 @@ createApp({ if (nextValue === "database") { this.ensureDatabaseWorkbenchLoaded(); } + if (nextValue === "release") { + this.ensureReleaseWorkbenchLoaded(); + } if (nextValue === "config") { this.configTab = this.configTab || "nacos"; } @@ -2018,6 +1994,28 @@ createApp({ }, }, methods: { + async ensureReleaseWorkbenchLoaded({ force = false } = {}) { + const shouldRefreshUpdate = force + || ((!this.updateOps.loaded || this.updateOps.items.length === 0 || Boolean(this.updateOps.error)) + && !this.updateOps.loading); + const shouldRefreshRestart = force + || ((!this.restartOps.loaded || this.restartOps.items.length === 0 || Boolean(this.restartOps.error)) + && !this.restartOps.loading); + + const jobs = []; + if (shouldRefreshUpdate) { + jobs.push(this.refreshUpdateTargets()); + } + if (shouldRefreshRestart) { + jobs.push(this.refreshRestartTargets()); + } + + if (jobs.length > 0) { + await Promise.allSettled(jobs); + } + + this.runReleasePrecheck(); + }, async ensureDatabaseWorkbenchLoaded(engine = this.databaseEngine) { if (engine === "mongo") { if (!this.mongoAdmin.loaded && !this.mongoAdmin.loading) { @@ -2233,9 +2231,7 @@ createApp({ this.recordReleaseHistory("precheck", this.releasePrecheck.title, checks.map((item) => `${item.label}: ${item.detail}`).join(" | "), ok ? "ok" : "warn"); }, async refreshReleaseTargets() { - await this.refreshUpdateTargets(); - await this.refreshRestartTargets(); - this.runReleasePrecheck(); + await this.ensureReleaseWorkbenchLoaded({ force: true }); }, toggleConfigGroup(groupKey) { const next = new Set(this.collapsedConfigGroups); @@ -5455,7 +5451,11 @@ createApp({ this.restartOps.error = error instanceof Error ? error.message : String(error); this.recordReleaseHistory("restart", "刷新滚动重启目标失败", this.explainError(this.restartOps.error), "bad"); } finally { + this.restartOps.loaded = true; this.restartOps.loading = false; + if (this.activeView === "release" && !this.updateOps.loading) { + this.runReleasePrecheck(); + } } }, async refreshUpdateTargets() { @@ -5497,7 +5497,11 @@ createApp({ this.updateOps.error = error instanceof Error ? error.message : String(error); this.recordReleaseHistory("update", "刷新更新目标失败", this.explainError(this.updateOps.error), "bad"); } finally { + this.updateOps.loaded = true; this.updateOps.loading = false; + if (this.activeView === "release" && !this.restartOps.loading) { + this.runReleasePrecheck(); + } } }, toggleUpdateService(serviceName) { diff --git a/static/index.html b/static/index.html index 6de83ec..0e8f72b 100644 --- a/static/index.html +++ b/static/index.html @@ -117,8 +117,7 @@