feat: add yumi admin deploy

This commit is contained in:
hy 2026-04-24 14:46:19 +08:00
parent 39ebdb4178
commit 2d81e98b3b
8 changed files with 654 additions and 215 deletions

View File

@ -52,14 +52,14 @@ UPDATE_DEFAULT_GO_GIT_REF=main
UPDATE_DEFAULT_ADMIN_GIT_REF=main UPDATE_DEFAULT_ADMIN_GIT_REF=main
JAVA_REPO_ROOT=/opt/deploy/sources/chatapp3-java JAVA_REPO_ROOT=/opt/deploy/sources/chatapp3-java
GOLANG_REPO_ROOT=/opt/deploy/sources/chatapp3-golang GOLANG_REPO_ROOT=/opt/deploy/sources/chatapp3-golang
ADMIN_FRONTEND_ENABLED=0 ADMIN_FRONTEND_ENABLED=1
ADMIN_FRONTEND_SERVICE_NAME=admin ADMIN_FRONTEND_SERVICE_NAME=admin
ADMIN_FRONTEND_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-admin.git ADMIN_FRONTEND_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-admin.git
ADMIN_FRONTEND_REPO_ROOT=/opt/deploy/sources/chatapp3-admin ADMIN_FRONTEND_REPO_ROOT=/opt/deploy/sources/chatapp3-admin
ADMIN_FRONTEND_HOST= ADMIN_FRONTEND_HOST=admin-platform
ADMIN_FRONTEND_IP= ADMIN_FRONTEND_IP=10.2.1.2
ADMIN_FRONTEND_INSTANCE_ID= ADMIN_FRONTEND_INSTANCE_ID=ins-p11begwe
ADMIN_FRONTEND_SSH_HOST= ADMIN_FRONTEND_SSH_HOST=10.2.1.2
ADMIN_FRONTEND_SSH_USER=root ADMIN_FRONTEND_SSH_USER=root
ADMIN_FRONTEND_SSH_PORT=22 ADMIN_FRONTEND_SSH_PORT=22
ADMIN_FRONTEND_SSH_USE_SUDO=0 ADMIN_FRONTEND_SSH_USE_SUDO=0
@ -70,7 +70,7 @@ ADMIN_FRONTEND_COMPOSE_SERVICE=caddy
ADMIN_FRONTEND_RELEASE_META_PATH=/opt/yumi-admin/frontend-admin/.hy-app-monitor-admin-release.json ADMIN_FRONTEND_RELEASE_META_PATH=/opt/yumi-admin/frontend-admin/.hy-app-monitor-admin-release.json
ADMIN_FRONTEND_BUILD_SCRIPT=build:antdv-next ADMIN_FRONTEND_BUILD_SCRIPT=build:antdv-next
ADMIN_FRONTEND_DIST_DIR=apps/dist ADMIN_FRONTEND_DIST_DIR=apps/dist
ADMIN_FRONTEND_PUBLIC_URL= ADMIN_FRONTEND_PUBLIC_URL=https://yumi-admin.haiyihy.com
MONGO_URI= MONGO_URI=
MONGO_DB_NAME=tarab_all MONGO_DB_NAME=tarab_all
MONGO_INSTANCE_ID=ins-o9mmob66 MONGO_INSTANCE_ID=ins-o9mmob66

View File

@ -391,9 +391,9 @@ body.auth-page {
.control-bar, .control-bar,
.panel { .panel {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 10px; border-radius: 8px;
background: linear-gradient(180deg, var(--panel-tint) 0%, var(--panel) 100%); background: linear-gradient(180deg, var(--panel-tint) 0%, var(--panel) 100%);
box-shadow: var(--shadow); box-shadow: none;
} }
.topbar { .topbar {
@ -401,14 +401,14 @@ body.auth-page {
top: 10px; top: 10px;
z-index: 20; z-index: 20;
display: grid; display: grid;
grid-template-columns: minmax(188px, 250px) minmax(0, max-content) minmax(420px, 1fr); grid-template-columns: minmax(170px, 230px) minmax(0, max-content) minmax(360px, 1fr);
gap: 10px; gap: 8px;
align-items: center; align-items: center;
min-height: 56px; min-height: 56px;
padding: 9px 14px; padding: 8px 12px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
box-shadow: box-shadow:
0 16px 32px rgba(33, 46, 76, 0.08), 0 10px 22px rgba(33, 46, 76, 0.05),
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent); inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
} }
@ -467,12 +467,52 @@ body.auth-page {
font-size: 11px; font-size: 11px;
} }
.topbar-nav { .app-switcher {
display: flex; display: inline-flex;
gap: 10px;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 4px; gap: 3px;
width: fit-content;
padding: 2px;
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
border-radius: 8px;
background: color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong));
}
.app-switch-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 8px;
border: 1px solid transparent;
border-radius: 6px;
color: var(--muted);
font-size: 10.5px;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
transition: color 180ms ease, border-color 180ms ease, background 180ms ease;
}
.app-switch-link:hover {
color: var(--text);
border-color: color-mix(in oklab, var(--accent) 20%, var(--line));
background: color-mix(in oklab, var(--panel-strong) 92%, var(--accent-soft));
}
.app-switch-link.active {
border-color: color-mix(in oklab, var(--accent) 38%, var(--line-strong));
background: color-mix(in oklab, var(--accent-soft) 62%, var(--panel-strong));
color: var(--accent);
}
.topbar-nav {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
padding: 3px;
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong)); border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
border-radius: 10px; border-radius: 10px;
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%);
@ -537,7 +577,8 @@ body.auth-page {
.topbar-status { .topbar-status {
justify-content: flex-end; justify-content: flex-end;
flex-wrap: nowrap; flex-wrap: nowrap;
padding: 4px; gap: 6px;
padding: 3px;
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong)); border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
border-radius: 10px; border-radius: 10px;
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%);
@ -551,6 +592,12 @@ body.auth-page {
white-space: nowrap; white-space: nowrap;
} }
.topbar-nav .nav-chip,
.topbar-status .refresh,
.topbar-status .interval-select {
min-height: 30px;
}
.control-bar { .control-bar {
position: sticky; position: sticky;
top: 72px; top: 72px;
@ -561,9 +608,7 @@ body.auth-page {
margin-top: 8px; margin-top: 8px;
padding: 8px 14px; padding: 8px 14px;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
box-shadow: box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
0 12px 26px rgba(33, 46, 76, 0.06),
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
} }
.task-control-bar { .task-control-bar {
@ -608,6 +653,7 @@ body.auth-page {
.section-link { .section-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 7px;
min-height: 28px; min-height: 28px;
padding: 0 10px; padding: 0 10px;
border: 1px solid transparent; border: 1px solid transparent;
@ -622,6 +668,43 @@ body.auth-page {
transition: color 180ms ease, opacity 180ms ease, border-color 180ms ease, background 180ms ease; transition: color 180ms ease, opacity 180ms ease, border-color 180ms ease, background 180ms ease;
} }
.release-step-link {
border-color: color-mix(in oklab, var(--line) 86%, var(--panel-strong));
background: color-mix(in oklab, var(--panel-strong) 90%, var(--panel-muted));
}
.release-step-index {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-size: 9px;
font-weight: 800;
letter-spacing: 0;
}
.release-step-link.tone-ok .release-step-index {
border-color: color-mix(in oklab, var(--ok) 34%, var(--line));
background: color-mix(in oklab, var(--ok-soft) 46%, var(--panel-strong));
color: color-mix(in oklab, var(--ok) 70%, var(--text));
}
.release-step-link.tone-warn .release-step-index {
border-color: color-mix(in oklab, var(--warn) 38%, var(--line));
background: color-mix(in oklab, var(--warn-soft) 56%, var(--panel-strong));
color: color-mix(in oklab, var(--warn) 82%, var(--text));
}
.release-step-link.tone-bad .release-step-index {
border-color: color-mix(in oklab, var(--bad) 42%, var(--line));
background: color-mix(in oklab, var(--bad-soft) 58%, var(--panel-strong));
color: color-mix(in oklab, var(--bad) 82%, var(--text));
}
.section-link:hover { .section-link:hover {
color: var(--text); color: var(--text);
border-color: color-mix(in oklab, var(--accent) 18%, var(--line)); border-color: color-mix(in oklab, var(--accent) 18%, var(--line));
@ -651,8 +734,8 @@ body.auth-page {
.topbar-search { .topbar-search {
flex: 1 1 220px; flex: 1 1 220px;
min-width: min(24vw, 300px); min-width: min(22vw, 260px);
max-width: 360px; max-width: 320px;
} }
.search::placeholder { .search::placeholder {
@ -723,6 +806,7 @@ body.auth-page {
.nav-chip:focus-visible, .nav-chip:focus-visible,
.filter-chip:focus-visible, .filter-chip:focus-visible,
.app-switch-link:focus-visible,
.refresh:focus-visible, .refresh:focus-visible,
.search:focus-visible, .search:focus-visible,
.interval-select:focus-visible, .interval-select:focus-visible,
@ -745,9 +829,7 @@ body.auth-page {
position: relative; position: relative;
padding: 14px; padding: 14px;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
var(--shadow),
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
} }
.matrix-panel, .matrix-panel,
@ -784,11 +866,11 @@ body.auth-page {
.summary-strip { .summary-strip {
margin-top: 8px; margin-top: 8px;
padding: 8px 10px; padding: 8px 10px;
background: background: var(--panel);
linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 95%, var(--accent-soft)) 0%, var(--panel) 100%);
} }
.summary-strip .status-band-list { .summary-strip .status-band-list {
border-radius: 8px;
box-shadow: none; box-shadow: none;
} }
@ -1041,6 +1123,20 @@ body.auth-page {
background: color-mix(in oklab, var(--ok-soft) 70%, var(--panel-strong)); background: color-mix(in oklab, var(--ok-soft) 70%, var(--panel-strong));
} }
.summary-strip .status-band-item.tone-ok {
background:
linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 99%, var(--accent-soft)) 0%, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 100%);
}
.summary-strip .status-band-item.tone-ok::before {
background: color-mix(in oklab, var(--ok) 28%, var(--line-strong));
opacity: 0.55;
}
.summary-strip .status-band-item.tone-ok strong {
color: color-mix(in oklab, var(--text) 74%, var(--ok));
}
.summary-tile.tone-warn, .summary-tile.tone-warn,
.matrix-state.tone-warn, .matrix-state.tone-warn,
.metric-chip.tone-warn, .metric-chip.tone-warn,
@ -1179,27 +1275,27 @@ body.auth-page {
.matrix-stacks { .matrix-stacks {
display: grid; display: grid;
gap: 12px; gap: 8px;
} }
.matrix-stack { .matrix-stack {
--group-color: var(--accent); --group-color: var(--accent);
display: grid; display: grid;
gap: 10px; gap: 6px;
padding: 10px 0 0; padding: 6px 0 0;
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong)); border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
border-radius: 16px; border-radius: 8px;
background: background:
linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%); linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%);
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent); box-shadow: none;
} }
.matrix-stack::before { .matrix-stack::before {
content: ""; content: "";
display: block; display: block;
width: 64px; width: 54px;
height: 3px; height: 2px;
border-radius: 999px; border-radius: 0 999px 999px 0;
background: linear-gradient(90deg, var(--group-color) 0%, color-mix(in oklab, var(--group-color) 50%, transparent) 100%); background: linear-gradient(90deg, var(--group-color) 0%, color-mix(in oklab, var(--group-color) 50%, transparent) 100%);
} }
@ -1208,7 +1304,7 @@ body.auth-page {
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
padding: 0 12px; padding: 0 10px 4px;
} }
.matrix-stack-headline { .matrix-stack-headline {
@ -1262,18 +1358,20 @@ body.auth-page {
} }
.matrix-table { .matrix-table {
min-width: 520px; width: max-content;
table-layout: fixed;
min-width: 460px;
} }
.matrix-table th, .matrix-table th,
.matrix-table td, .matrix-table td,
.table th, .table th,
.table td { .table td {
padding: 6px 9px; padding: 5px 8px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
text-align: left; text-align: left;
vertical-align: middle; vertical-align: middle;
font-size: 11px; font-size: 10.5px;
} }
.matrix-table th, .matrix-table th,
@ -1287,6 +1385,10 @@ body.auth-page {
white-space: nowrap; white-space: nowrap;
} }
.matrix-table th:not(.matrix-sticky-col) {
width: 92px;
}
.table tbody tr:nth-child(even), .table tbody tr:nth-child(even),
.matrix-table tbody tr:nth-child(even) { .matrix-table tbody tr:nth-child(even) {
background: color-mix(in oklab, var(--accent-soft) 10%, var(--panel-strong)); background: color-mix(in oklab, var(--accent-soft) 10%, var(--panel-strong));
@ -1321,19 +1423,20 @@ body.auth-page {
} }
.matrix-service strong { .matrix-service strong {
font-size: 12px; font-size: 11.5px;
line-height: 1.3; line-height: 1.3;
} }
.matrix-kind { .matrix-kind {
color: var(--muted); color: var(--muted);
font-size: 11px; font-size: 10px;
} }
.matrix-state { .matrix-state {
min-width: 34px; min-width: 28px;
height: 24px; width: 92px;
padding: 0 4px; height: 22px;
padding: 0 3px;
border-left: 1px solid var(--line); border-left: 1px solid var(--line);
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
@ -1341,12 +1444,12 @@ body.auth-page {
.matrix-state-marker { .matrix-state-marker {
display: block; display: block;
width: 9px; width: 7px;
height: 9px; height: 7px;
margin: 0 auto; margin: 0 auto;
border-radius: 999px; border-radius: 999px;
background: currentColor; background: currentColor;
box-shadow: 0 0 0 3px color-mix(in oklab, currentColor 12%, transparent); box-shadow: 0 0 0 2px color-mix(in oklab, currentColor 12%, transparent);
} }
.matrix-state-code { .matrix-state-code {
@ -1366,8 +1469,8 @@ body.auth-page {
} }
.matrix-table.is-detail .matrix-state { .matrix-table.is-detail .matrix-state {
height: 40px; height: 34px;
padding: 4px 5px; padding: 3px 4px;
} }
.matrix-table.is-detail .matrix-state-code, .matrix-table.is-detail .matrix-state-code,
@ -1401,7 +1504,8 @@ body.auth-page {
z-index: 3; z-index: 3;
background: color-mix(in oklab, var(--panel-strong) 94%, var(--panel-muted)); background: color-mix(in oklab, var(--panel-strong) 94%, var(--panel-muted));
box-shadow: 1px 0 0 var(--line); box-shadow: 1px 0 0 var(--line);
min-width: 118px; min-width: 108px;
width: 136px;
} }
.matrix-table thead .matrix-sticky-col { .matrix-table thead .matrix-sticky-col {
@ -2379,6 +2483,7 @@ body.auth-page {
.config-workbench-panel { .config-workbench-panel {
position: relative; position: relative;
padding: 10px 12px;
background: var(--panel); background: var(--panel);
box-shadow: none; box-shadow: none;
} }
@ -2388,8 +2493,8 @@ body.auth-page {
} }
.config-head-strip { .config-head-strip {
margin-bottom: 12px; margin-bottom: 8px;
padding: 0 0 10px; padding: 0 0 7px;
border: 0; border: 0;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
border-radius: 0; border-radius: 0;
@ -2401,8 +2506,8 @@ body.auth-page {
} }
.config-inline-strip { .config-inline-strip {
margin-bottom: 10px; margin-bottom: 8px;
padding: 0 0 8px; padding: 0 0 6px;
border: 0; border: 0;
border-bottom: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong)); border-bottom: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
border-radius: 0; border-radius: 0;
@ -2589,11 +2694,11 @@ body.auth-page {
gap: 4px; gap: 4px;
align-self: start; align-self: start;
position: sticky; position: sticky;
top: 116px; top: 0;
max-height: calc(100vh - 128px); max-height: calc(100vh - 128px);
padding: 6px; padding: 6px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 14px; border-radius: 8px;
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent); box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
} }
@ -2601,7 +2706,7 @@ body.auth-page {
.config-only-layout > .config-sidebar { .config-only-layout > .config-sidebar {
gap: 6px; gap: 6px;
max-height: calc(100vh - 124px); max-height: calc(100vh - 124px);
padding: 0 10px 0 0; padding: 0 8px 0 0;
border: 0; border: 0;
border-right: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong)); border-right: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
border-radius: 0; border-radius: 0;
@ -2625,7 +2730,7 @@ body.auth-page {
} }
.config-list-ide { .config-list-ide {
gap: 4px; gap: 2px;
padding-right: 2px; padding-right: 2px;
} }
@ -2746,7 +2851,7 @@ body.auth-page {
.config-jump-group { .config-jump-group {
display: inline-flex; display: inline-flex;
gap: 8px; gap: 6px;
align-items: center; align-items: center;
} }
@ -2754,9 +2859,9 @@ body.auth-page {
display: grid; display: grid;
gap: 2px; gap: 2px;
min-width: 0; min-width: 0;
padding: 4px 7px; padding: 3px 6px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 11px; border-radius: 7px;
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, var(--panel-strong) 100%); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, var(--panel-strong) 100%);
} }
@ -2778,33 +2883,64 @@ body.auth-page {
.editor-toolbar { .editor-toolbar {
position: relative; position: relative;
display: flex; display: grid;
justify-content: space-between; grid-template-columns: minmax(0, 1fr) auto;
gap: 12px; gap: 10px;
align-items: center; align-items: center;
padding: 6px; padding: 5px;
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong)); border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
border-radius: 14px; border-radius: 8px;
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent); box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
} }
.sticky-toolbar { .sticky-toolbar {
position: sticky; position: sticky;
top: 124px; top: 0;
z-index: 8; z-index: 8;
} }
.editor-toolbar-main { .editor-toolbar-main {
display: flex; display: flex;
gap: 8px; gap: 6px;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
.editor-toolbar-meta {
grid-column: 1 / -1;
display: flex;
gap: 8px;
flex-wrap: wrap;
padding-top: 4px;
border-top: 1px solid color-mix(in oklab, var(--line) 82%, var(--panel-strong));
color: var(--muted);
font-size: 9.5px;
}
.editor-toolbar-meta span {
overflow: hidden;
max-width: 220px;
text-overflow: ellipsis;
white-space: nowrap;
}
.editor-toolbar .filter-chip,
.editor-toolbar .refresh.ghost {
min-height: 28px;
padding: 0 9px;
border-color: color-mix(in oklab, var(--line) 80%, transparent);
background: transparent;
box-shadow: none;
}
.editor-toolbar .refresh:not(.ghost) {
min-height: 30px;
}
.diff-panel { .diff-panel {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 16px; border-radius: 8px;
background: oklch(100% 0 0 / 0.86); background: oklch(100% 0 0 / 0.86);
overflow: auto; overflow: auto;
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent); box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
@ -2946,7 +3082,7 @@ body.auth-page {
.release-status-strip { .release-status-strip {
margin-top: 8px; margin-top: 8px;
padding: 12px; padding: 10px 12px;
background: var(--panel); background: var(--panel);
box-shadow: none; box-shadow: none;
} }
@ -2962,7 +3098,7 @@ body.auth-page {
justify-content: space-between; justify-content: space-between;
gap: 14px; gap: 14px;
align-items: flex-start; align-items: flex-start;
padding-bottom: 10px; padding-bottom: 8px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
} }
@ -3000,11 +3136,11 @@ body.auth-page {
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
margin-top: 10px; margin-top: 8px;
} }
.release-meta-strip { .release-meta-strip {
margin-top: 10px; margin-top: 8px;
align-items: stretch; align-items: stretch;
} }
@ -3050,10 +3186,28 @@ body.auth-page {
margin-top: 8px; margin-top: 8px;
} }
.release-command-row {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
}
.release-command-actions .refresh {
min-height: 30px;
}
.release-command-actions .refresh.ghost {
background: transparent;
box-shadow: none;
}
.release-bottom-action-bar { .release-bottom-action-bar {
position: sticky; position: static;
bottom: 12px; z-index: auto;
z-index: 16;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
@ -3061,11 +3215,9 @@ body.auth-page {
margin-top: 12px; margin-top: 12px;
padding: 8px 10px; padding: 8px 10px;
border: 1px solid var(--line-strong); border: 1px solid var(--line-strong);
border-radius: 10px; border-radius: 8px;
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--accent-soft)) 0%, var(--panel-strong) 100%); background: var(--panel-strong);
box-shadow: box-shadow: none;
0 14px 30px rgba(33, 46, 76, 0.12),
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 78%, transparent);
} }
.release-action-summary.compact { .release-action-summary.compact {
@ -3074,19 +3226,19 @@ body.auth-page {
.release-card-grid { .release-card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 10px; gap: 7px;
margin-top: 10px; margin-top: 8px;
} }
.release-service-card { .release-service-card {
--release-kind-color: var(--accent); --release-kind-color: var(--accent);
position: relative; position: relative;
display: grid; display: grid;
gap: 10px; gap: 6px;
padding: 10px 12px; padding: 8px 10px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 12px; border-radius: 8px;
background: var(--panel-strong); background: var(--panel-strong);
cursor: pointer; cursor: pointer;
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease; transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
@ -3097,8 +3249,8 @@ body.auth-page {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
top: 8px; top: 7px;
bottom: 8px; bottom: 7px;
width: 3px; width: 3px;
border-radius: 999px; border-radius: 999px;
background: var(--release-kind-color); background: var(--release-kind-color);
@ -3143,52 +3295,59 @@ body.auth-page {
.release-service-head { .release-service-head {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 8px;
align-items: flex-start; align-items: flex-start;
} }
.release-service-title { .release-service-title {
display: grid; display: grid;
gap: 6px; gap: 4px;
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
} }
.release-service-title-line { .release-service-title-line {
display: flex; display: flex;
gap: 8px; gap: 5px;
flex-wrap: wrap; flex-wrap: nowrap;
align-items: center; align-items: center;
} }
.release-service-title strong { .release-service-title strong {
overflow: hidden;
flex: 1 1 auto;
font-size: 13px; font-size: 13px;
line-height: 1.3; line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
} }
.release-service-title-line .badge { .release-service-title-line .badge {
flex: 0 0 auto;
min-height: 22px; min-height: 22px;
padding: 0 8px; padding: 0 6px;
font-size: 10px; font-size: 9.5px;
} }
.release-service-meta { .release-service-meta {
display: flex; display: grid;
flex-wrap: wrap; gap: 2px;
gap: 6px 12px;
color: var(--muted); color: var(--muted);
font-size: 11px; font-size: 10px;
line-height: 1.45; line-height: 1.25;
} }
.release-service-meta span { .release-service-meta span {
overflow: hidden;
min-width: 0; min-width: 0;
word-break: break-word; text-overflow: ellipsis;
white-space: nowrap;
word-break: normal;
} }
.release-service-actions { .release-service-actions {
display: grid; display: grid;
gap: 6px; gap: 3px;
justify-items: end; justify-items: end;
align-self: center; align-self: center;
} }
@ -3198,18 +3357,22 @@ body.auth-page {
} }
.release-service-action-row { .release-service-action-row {
display: flex; display: none;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
} }
.release-service-card[data-kind="frontend"] .release-service-action-row {
display: flex;
}
.release-select-state { .release-select-state {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
min-height: 24px; min-height: 22px;
padding: 0 9px; padding: 0 7px;
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong)); border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
border-radius: 999px; border-radius: 999px;
background: color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong)); background: color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong));
@ -3226,8 +3389,7 @@ body.auth-page {
} }
.release-service-note { .release-service-note {
color: var(--muted); display: none;
margin: 0;
} }
.precheck-strip { .precheck-strip {
@ -3655,8 +3817,8 @@ body.auth-page {
.config-layout { .config-layout {
display: grid; display: grid;
grid-template-columns: minmax(208px, 236px) minmax(0, 1fr); grid-template-columns: minmax(176px, 216px) minmax(0, 1fr);
gap: 10px; gap: 8px;
} }
.infra-panel > .panel-header { .infra-panel > .panel-header {
@ -3719,6 +3881,53 @@ body.auth-page {
margin-top: 6px; margin-top: 6px;
} }
.config-list-ide .config-item-ide {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 6px;
align-items: center;
min-height: 28px;
padding: 4px 6px 4px 8px;
border: 0;
border-radius: 6px;
background: transparent;
box-shadow: none;
}
.config-list-ide .config-item-ide:hover {
transform: none;
background: color-mix(in oklab, var(--accent-soft) 16%, var(--panel-strong));
}
.config-list-ide .config-item-ide.active {
border-color: transparent;
background: color-mix(in oklab, var(--accent-soft) 56%, var(--panel-strong));
box-shadow: inset 2px 0 0 var(--accent);
}
.config-list-ide .config-item-ide strong {
overflow: hidden;
color: var(--text);
font-size: 10.5px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
word-break: normal;
}
.config-list-ide .config-item-ide small {
display: none;
}
.config-list-ide .config-item-ide span {
margin-top: 0;
color: var(--muted);
font-size: 9px;
line-height: 1;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.config-description { .config-description {
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -3728,7 +3937,11 @@ body.auth-page {
.config-editor { .config-editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 8px;
}
.config-editor-meta {
margin-bottom: 0;
} }
.config-empty, .config-empty,
@ -3771,10 +3984,10 @@ body.auth-page {
.config-textarea { .config-textarea {
width: 100%; width: 100%;
min-height: clamp(320px, 60vh, 680px); min-height: clamp(360px, 66vh, 760px);
padding: 12px 14px; padding: 12px 14px;
resize: vertical; resize: vertical;
border-radius: 14px; border-radius: 8px;
background: oklch(100% 0 0 / 0.86); background: oklch(100% 0 0 / 0.86);
box-shadow: inset 0 1px 2px rgba(52, 71, 103, 0.08); box-shadow: inset 0 1px 2px rgba(52, 71, 103, 0.08);
font-size: 12px; font-size: 12px;
@ -4685,11 +4898,30 @@ body.auth-page {
} }
} }
@media (max-width: 1280px) {
.topbar {
grid-template-columns: minmax(160px, 220px) minmax(0, 1fr);
}
.topbar-status {
grid-column: 1 / -1;
justify-content: flex-start;
}
.topbar-search {
max-width: none;
}
}
@media (max-width: 1040px) { @media (max-width: 1040px) {
.control-bar { .control-bar {
position: static; position: static;
} }
.topbar {
grid-template-columns: 1fr;
}
.topbar-nav, .topbar-nav,
.topbar-status { .topbar-status {
width: 100%; width: 100%;
@ -4901,6 +5133,7 @@ body.auth-page {
} }
.release-action-bar, .release-action-bar,
.release-command-row,
.release-bottom-action-bar, .release-bottom-action-bar,
.release-task-head, .release-task-head,
.log-viewer-head, .log-viewer-head,

View File

@ -6,6 +6,11 @@ const APP_CONTEXTS = [
{ id: "common", basePath: "/common", mode: "common", title: "HY App Monitor 通用" }, { id: "common", basePath: "/common", mode: "common", title: "HY App Monitor 通用" },
{ id: "mysql", basePath: "/mysql", mode: "common", title: "HY App Monitor 通用" }, { id: "mysql", basePath: "/mysql", mode: "common", title: "HY App Monitor 通用" },
]; ];
const APP_SWITCH_LINKS = [
{ id: "yumi", label: "Yumi", href: "/yumi/" },
{ id: "haiyi", label: "HaiYi", href: "/haiyi/" },
{ id: "common", label: "通用", href: "/common/" },
];
const STORAGE_KEY = "hy-app-monitor-refresh-interval"; const STORAGE_KEY = "hy-app-monitor-refresh-interval";
const UPDATE_OPERATION_STORAGE_KEY = "hy-app-monitor-service-update-operation"; const UPDATE_OPERATION_STORAGE_KEY = "hy-app-monitor-service-update-operation";
const GROUP_LABELS = { const GROUP_LABELS = {
@ -402,6 +407,8 @@ createApp({
const isCommonMode = CURRENT_APP_CONTEXT.mode === "common"; const isCommonMode = CURRENT_APP_CONTEXT.mode === "common";
return { return {
activeView: isCommonMode ? "common" : "overview", activeView: isCommonMode ? "common" : "overview",
appSwitchLinks: APP_SWITCH_LINKS,
currentAppEntryId: CURRENT_APP_CONTEXT.id === "mysql" ? "common" : CURRENT_APP_CONTEXT.id,
databaseEngine: isCommonMode ? "mysql" : "mongo", databaseEngine: isCommonMode ? "mysql" : "mongo",
commonTool: "mysql", commonTool: "mysql",
densityMode: "compact", densityMode: "compact",
@ -1727,47 +1734,14 @@ createApp({
goConfigDirty() { goConfigDirty() {
return this.goConfig.editorContent !== this.goConfig.loadedContent; return this.goConfig.editorContent !== this.goConfig.loadedContent;
}, },
allReleaseTargetRows() {
return (this.updateOps.items || [])
.map((item) => this.decorateReleaseTargetItem(item))
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
},
releaseTargetRows() { releaseTargetRows() {
const keyword = this.normalizedReleaseKeyword; const keyword = this.normalizedReleaseKeyword;
return (this.updateOps.items || []) return this.allReleaseTargetRows
.map((item) => {
const images = Array.isArray(item.currentImages) ? item.currentImages : [];
const primaryImage = images[0] || "";
const imageMeta = this.imageMeta(primaryImage);
const imageSummary = images.length > 1
? `${images.length} 个版本`
: primaryImage || "-";
const currentLabel = item.currentLabel || (images.length > 1 ? "多版本" : (imageMeta.tag || "-"));
const currentTitle = item.currentTitle || imageSummary;
const detailLabel = item.detailLabel || (images.length > 1 ? `${images.length}` : imageMeta.digestShort);
const detailTitle = item.detailTitle || primaryImage || "-";
const copyValue = item.copyValue || primaryImage || "";
return {
...item,
hostCount: (item.hosts || []).length,
hostsLabel: (item.hosts || []).map((host) => host.host).join(" / "),
imageTitle: images.join("\n"),
imageSummary,
currentLabel,
currentTitle,
detailLabel,
detailTitle,
copyValue,
digestFull: imageMeta.digestFull,
repositoryShort: item.repository ? this.truncateMiddle(item.repository, 18, 10) : imageMeta.repositoryShort,
currentImagePrimary: primaryImage,
currentImageCount: images.length,
searchText: [
item.service,
item.kind,
item.repository,
currentLabel,
detailLabel,
...(item.hosts || []).flatMap((host) => [host.host, host.ip, host.path, host.port]),
...images,
].join(" ").toLowerCase(),
};
})
.filter((item) => { .filter((item) => {
if (this.releaseKindFilter !== "all" && item.kind !== this.releaseKindFilter) { if (this.releaseKindFilter !== "all" && item.kind !== this.releaseKindFilter) {
return false; return false;
@ -1776,8 +1750,7 @@ createApp({
return false; return false;
} }
return true; return true;
}) });
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
}, },
restartTargetRows() { restartTargetRows() {
const keyword = this.normalizedReleaseKeyword; const keyword = this.normalizedReleaseKeyword;
@ -1816,13 +1789,13 @@ createApp({
}, },
{ {
label: "最近结果", label: "最近结果",
value: this.updateStatusLabel(), value: this.releasePrimaryStatusLabel(),
tone: this.updateStatusBadgeClass(), tone: this.releasePrimaryStatusTone(),
}, },
{ {
label: "失败阶段", label: "失败阶段",
value: this.updateOps.error ? (this.updateOps.stage || "failed") : "-", value: this.updateOps.error || this.restartOps.error ? (this.updateOps.stage || "failed") : "-",
tone: this.updateOps.error ? "bad" : "neutral", tone: this.updateOps.error || this.restartOps.error ? "bad" : "neutral",
}, },
{ {
label: "影响服务", label: "影响服务",
@ -2446,6 +2419,44 @@ createApp({
digestShort: digestFull ? this.truncateMiddle(digestFull, 10, 6) : "-", digestShort: digestFull ? this.truncateMiddle(digestFull, 10, 6) : "-",
}; };
}, },
decorateReleaseTargetItem(item) {
const images = Array.isArray(item.currentImages) ? item.currentImages : [];
const primaryImage = images[0] || "";
const imageMeta = this.imageMeta(primaryImage);
const imageSummary = images.length > 1
? `${images.length} 个版本`
: primaryImage || "-";
const currentLabel = item.currentLabel || (images.length > 1 ? "多版本" : (imageMeta.tag || "-"));
const currentTitle = item.currentTitle || imageSummary;
const detailLabel = item.detailLabel || (images.length > 1 ? `${images.length}` : imageMeta.digestShort);
const detailTitle = item.detailTitle || primaryImage || "-";
const copyValue = item.copyValue || primaryImage || "";
return {
...item,
hostCount: (item.hosts || []).length,
hostsLabel: (item.hosts || []).map((host) => host.host).join(" / "),
imageTitle: images.join("\n"),
imageSummary,
currentLabel,
currentTitle,
detailLabel,
detailTitle,
copyValue,
digestFull: imageMeta.digestFull,
repositoryShort: item.repository ? this.truncateMiddle(item.repository, 18, 10) : imageMeta.repositoryShort,
currentImagePrimary: primaryImage,
currentImageCount: images.length,
searchText: [
item.service,
item.kind,
item.repository,
currentLabel,
detailLabel,
...(item.hosts || []).flatMap((host) => [host.host, host.ip, host.path, host.port]),
...images,
].join(" ").toLowerCase(),
};
},
truncateMiddle(value, start = 12, end = 10) { truncateMiddle(value, start = 12, end = 10) {
const text = String(value || ""); const text = String(value || "");
if (!text) { if (!text) {
@ -3258,6 +3269,83 @@ createApp({
} }
return "未开始"; return "未开始";
}, },
releaseIsRunning() {
return Boolean(
this.restartOps.running
|| this.updateOps.running
|| this.updateOps.status === "queued"
|| this.updateOps.status === "running"
);
},
releasePrimaryStatusLabel() {
if (this.restartOps.running) {
return "重启执行中";
}
if (this.updateOps.status === "queued") {
return "更新排队中";
}
if (this.updateOps.running || this.updateOps.status === "running") {
return "更新执行中";
}
if (this.restartOps.error) {
return "重启失败";
}
if (this.updateOps.error || this.updateOps.status === "failed") {
return "更新失败";
}
if (this.restartOps.result?.ok) {
return "重启已完成";
}
if (this.updateOps.status === "success") {
return "更新已完成";
}
return "待操作";
},
releasePrimaryStatusTone() {
if (this.updateOps.error || this.restartOps.error || this.updateOps.status === "failed") {
return "bad";
}
if (this.releaseIsRunning()) {
return "warn";
}
if (this.updateOps.status === "success" || this.restartOps.result?.ok) {
return "ok";
}
return "neutral";
},
releasePrecheckBadgeLabel() {
if (!this.releasePrecheck.ranAt) {
return "未预检查";
}
return this.releaseIsRunning() ? `上次${this.releasePrecheck.title}` : this.releasePrecheck.title;
},
releaseStepTone(sectionId) {
if (sectionId === "release-build") {
return this.updateOps.selectedServices.length > 0 || this.updateOps.status ? "ok" : "neutral";
}
if (sectionId === "release-targets") {
return this.updateOps.selectedServices.length > 0 || this.restartOps.selectedServices.length > 0 ? "ok" : "neutral";
}
if (sectionId === "release-log") {
if (this.updateOps.error || this.updateOps.status === "failed") {
return "bad";
}
if (this.updateOps.running || this.updateOps.status === "queued" || this.updateOps.status === "running") {
return "warn";
}
return this.updateOps.status === "success" ? "ok" : "neutral";
}
if (sectionId === "release-restart") {
if (this.restartOps.error) {
return "bad";
}
if (this.restartOps.running) {
return "warn";
}
return this.restartOps.result?.ok || this.restartOps.selectedServices.length > 0 ? "ok" : "neutral";
}
return "neutral";
},
syncRefreshSettings() { syncRefreshSettings() {
const settings = this.payload.settings || {}; const settings = this.payload.settings || {};
const options = Array.isArray(settings.refreshIntervalOptions) const options = Array.isArray(settings.refreshIntervalOptions)
@ -5974,11 +6062,14 @@ createApp({
} }
this.updateOps.selectedServices = [...next]; this.updateOps.selectedServices = [...next];
}, },
async runServiceUpdate() { async runServiceUpdate(serviceNames = this.updateOps.selectedServices) {
if (this.updateOps.selectedServices.length === 0) { const requestedServices = Array.isArray(serviceNames) ? serviceNames : this.updateOps.selectedServices;
const services = [...new Set((requestedServices || []).map((service) => String(service || "").trim()).filter(Boolean))];
if (services.length === 0) {
return; return;
} }
this.updateOps.selectedServices = services;
this.stopUpdatePolling(); this.stopUpdatePolling();
this.updateOps.requestEpoch += 1; this.updateOps.requestEpoch += 1;
this.updateOps.statusRequestInFlight = false; this.updateOps.statusRequestInFlight = false;
@ -5995,7 +6086,7 @@ createApp({
this.updateOps.logs = []; this.updateOps.logs = [];
this.updateOps.result = null; this.updateOps.result = null;
this.persistUpdateOperationId(); this.persistUpdateOperationId();
this.recordReleaseHistory("update", "提交服务更新", this.updateOps.selectedServices.join(", "), "warn"); this.recordReleaseHistory("update", "提交服务更新", services.join(", "), "warn");
try { try {
const { response, payload } = await this.requestJson("/api/ops/update-services", { const { response, payload } = await this.requestJson("/api/ops/update-services", {
@ -6004,7 +6095,7 @@ createApp({
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
services: this.updateOps.selectedServices, services,
javaGitRef: this.updateOps.javaGitRef, javaGitRef: this.updateOps.javaGitRef,
goGitRef: this.updateOps.goGitRef, goGitRef: this.updateOps.goGitRef,
adminGitRef: this.updateOps.adminGitRef, adminGitRef: this.updateOps.adminGitRef,

View File

@ -37,7 +37,9 @@
</div> </div>
<div class="entry-actions"> <div class="entry-actions">
<a class="refresh ghost entry-link" href="/select">返回入口</a> <a class="refresh ghost entry-link" href="/yumi/">Yumi</a>
<a class="refresh ghost entry-link" href="/common/">通用</a>
<a class="refresh ghost entry-link" href="/select">选择入口</a>
<button id="haiyi-refresh" class="refresh" type="button">刷新</button> <button id="haiyi-refresh" class="refresh" type="button">刷新</button>
<button id="haiyi-logout" class="refresh ghost" type="button">退出</button> <button id="haiyi-logout" class="refresh ghost" type="button">退出</button>
</div> </div>

View File

@ -24,6 +24,16 @@
<span class="env-pill">DEPLOY</span> <span class="env-pill">DEPLOY</span>
</div> </div>
<p class="sync-note">最后刷新 {{ formattedUpdatedAt }}</p> <p class="sync-note">最后刷新 {{ formattedUpdatedAt }}</p>
<nav class="app-switcher" aria-label="入口切换">
<a
v-for="entry in appSwitchLinks"
:key="entry.id"
:href="entry.href"
:class="['app-switch-link', currentAppEntryId === entry.id ? 'active' : '']"
>
{{ entry.label }}
</a>
</nav>
</div> </div>
<nav v-if="viewTabs.length > 1" class="topbar-nav" aria-label="主导航"> <nav v-if="viewTabs.length > 1" class="topbar-nav" aria-label="主导航">
@ -41,10 +51,9 @@
<span :class="['badge', globalHealthTone]">{{ globalHealthLabel }}</span> <span :class="['badge', globalHealthTone]">{{ globalHealthLabel }}</span>
<input v-model.trim="globalSearchModel" class="search topbar-search" :placeholder="globalSearchPlaceholder" :disabled="globalSearchDisabled" /> <input v-model.trim="globalSearchModel" class="search topbar-search" :placeholder="globalSearchPlaceholder" :disabled="globalSearchDisabled" />
<span v-if="auth.username" class="badge neutral mono">{{ auth.username }}</span> <span v-if="auth.username" class="badge neutral mono">{{ auth.username }}</span>
<span :class="['badge', loading ? 'warn' : 'neutral']">{{ loading ? '刷新中' : '在线' }}</span> <span v-if="loading" class="badge warn">刷新中</span>
<span v-if="error" class="badge bad">接口异常</span> <span v-if="error" class="badge bad">接口异常</span>
<label class="interval-field topbar-interval"> <label class="interval-field topbar-interval" aria-label="刷新间隔">
<span>刷新</span>
<select v-model.number="refreshIntervalSeconds" class="interval-select"> <select v-model.number="refreshIntervalSeconds" class="interval-select">
<option v-for="seconds in refreshIntervalOptions" :key="seconds" :value="seconds">{{ seconds }}s</option> <option v-for="seconds in refreshIntervalOptions" :key="seconds" :value="seconds">{{ seconds }}s</option>
</select> </select>
@ -304,12 +313,13 @@
<div class="control-group release-section-nav"> <div class="control-group release-section-nav">
<button <button
v-for="section in releaseSections" v-for="(section, index) in releaseSections"
:key="section.id" :key="section.id"
class="section-link" :class="['section-link', 'release-step-link', `tone-${releaseStepTone(section.id)}`]"
@click="scrollToSection(section.id)" @click="scrollToSection(section.id)"
> >
{{ section.label }} <span class="release-step-index">{{ index + 1 }}</span>
<span>{{ section.label }}</span>
</button> </button>
</div> </div>
@ -2980,14 +2990,13 @@
{{ nacosConfig.saving ? '保存中...' : '保存' }} {{ nacosConfig.saving ? '保存中...' : '保存' }}
</button> </button>
</div> </div>
</div> <div class="editor-toolbar-meta">
<span>最近修改 {{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</span>
<div class="metadata-strip metadata-strip-tight config-inline-strip"> <span>{{ nacosConfig.validationMessage ? '已校验' : '未校验' }}</span>
<span>最近修改 {{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</span> <span v-if="nacosConfig.editor.appName">app {{ nacosConfig.editor.appName }}</span>
<span>{{ nacosConfig.validationMessage ? '已校验' : '未校验' }}</span> <span v-if="nacosConfig.editor.description">{{ nacosConfig.editor.description }}</span>
<span v-if="nacosConfig.editor.appName">app {{ nacosConfig.editor.appName }}</span> <span v-if="nacosConfig.editor.md5" class="mono">{{ truncateMiddle(nacosConfig.editor.md5, 8, 8) }}</span>
<span v-if="nacosConfig.editor.description">{{ nacosConfig.editor.description }}</span> </div>
<span v-if="nacosConfig.editor.md5" class="mono">{{ truncateMiddle(nacosConfig.editor.md5, 8, 8) }}</span>
</div> </div>
<section v-if="configEditorMode === 'diff'" class="diff-panel"> <section v-if="configEditorMode === 'diff'" class="diff-panel">
@ -3132,15 +3141,46 @@
</div> </div>
</div> </div>
<div class="release-action-summary"> <div class="release-command-row" aria-label="发布操作">
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span> <div class="release-action-summary compact">
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span> <span :class="['badge', releasePrimaryStatusTone()]">{{ releasePrimaryStatusLabel() }}</span>
<span class="badge neutral">重启目标 {{ restartTargetRows.length }}</span> <span class="badge neutral">更新 {{ updateOps.selectedServices.length }} / {{ releaseTargetRows.length }}</span>
<span class="badge neutral">已选更新 {{ updateOps.selectedServices.length }}</span> <span class="badge neutral">重启 {{ restartOps.selectedServices.length }} / {{ restartTargetRows.length }}</span>
<span class="badge neutral">已选重启 {{ restartOps.selectedServices.length }}</span> <span :class="['badge', releasePrecheck.ok ? 'ok' : releasePrecheck.ranAt ? 'warn' : 'neutral']">
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']"> {{ releasePrecheckBadgeLabel() }}
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }} </span>
</span> </div>
<div class="action-group release-command-actions">
<template v-if="releaseIsRunning()">
<button class="refresh" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
<button class="refresh ghost" @click="fetchUpdateOperationStatus(updateOps.operationId)" :disabled="!updateOps.operationId || updateOps.statusRequestInFlight">
{{ updateOps.statusRequestInFlight ? '刷新中...' : '刷新状态' }}
</button>
<button class="refresh ghost" @click="stopUpdatePolling" :disabled="!updateOps.pollTimer">停止轮询</button>
<span class="badge warn">等待完成</span>
</template>
<template v-else>
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading">
{{ updateOps.loading || restartOps.loading ? '刷新中...' : '刷新目标' }}
</button>
<button class="refresh ghost" @click="runReleasePrecheck">预检查</button>
<button class="refresh ghost" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
<button
:class="['refresh', updateOps.selectedServices.length > 0 ? 'danger' : 'ghost']"
@click="runServiceUpdate"
:disabled="updateOps.selectedServices.length === 0"
>
{{ updateOps.selectedServices.length > 0 ? `执行更新 ${updateOps.selectedServices.length}` : '执行更新' }}
</button>
<button
:class="['refresh', restartOps.selectedServices.length > 0 ? 'danger' : 'ghost']"
@click="runRollingRestart"
:disabled="restartOps.selectedServices.length === 0"
>
{{ restartOps.selectedServices.length > 0 ? `执行重启 ${restartOps.selectedServices.length}` : '执行重启' }}
</button>
</template>
</div>
</div> </div>
<div class="metadata-strip metadata-strip-tight release-meta-strip"> <div class="metadata-strip metadata-strip-tight release-meta-strip">
@ -3155,7 +3195,7 @@
</div> </div>
<div v-if="releasePrecheck.ranAt" class="precheck-strip release-check-strip"> <div v-if="releasePrecheck.ranAt" class="precheck-strip release-check-strip">
<span :class="['badge', releasePrecheck.tone]">{{ releasePrecheck.title }}</span> <span :class="['badge', releasePrecheck.tone]">{{ releasePrecheckBadgeLabel() }}</span>
<span class="badge neutral">执行于 {{ formatDateTime(releasePrecheck.ranAt) }}</span> <span class="badge neutral">执行于 {{ formatDateTime(releasePrecheck.ranAt) }}</span>
<span <span
v-for="check in releasePrecheck.checks" v-for="check in releasePrecheck.checks"
@ -3168,7 +3208,7 @@
</div> </div>
<p v-if="updateOps.error" class="metric-error">{{ explainError(updateOps.error) }}</p> <p v-if="updateOps.error" class="metric-error">{{ explainError(updateOps.error) }}</p>
<p v-else-if="updateOps.message" :class="updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p> <p v-else-if="updateOps.message" :class="updateOps.running ? 'warn-text' : updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p>
</section> </section>
<section id="release-build" class="panel infra-panel"> <section id="release-build" class="panel infra-panel">
@ -3258,6 +3298,17 @@
</span> </span>
<small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small> <small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small>
<div class="release-service-action-row"> <div class="release-service-action-row">
<button
v-if="item.kind === 'frontend'"
class="refresh inline-button"
@click.stop="runServiceUpdate([item.service])"
@keydown.enter.stop
@keydown.space.stop
:disabled="releaseIsRunning() || !updateOps.adminGitRef.trim()"
:title="!updateOps.adminGitRef.trim() ? '先填写 Admin Git Ref' : ''"
>
{{ item.service === 'admin' ? 'admin部署' : item.service + '部署' }}
</button>
<button <button
v-if="item.copyValue" v-if="item.copyValue"
class="refresh ghost inline-button" class="refresh ghost inline-button"
@ -3478,29 +3529,6 @@
</details> </details>
</section> </section>
<section class="release-bottom-action-bar" aria-label="发布操作">
<div class="release-action-summary compact">
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
<span class="badge neutral">更新 {{ updateOps.selectedServices.length }} / {{ releaseTargetRows.length }}</span>
<span class="badge neutral">重启 {{ restartOps.selectedServices.length }} / {{ restartTargetRows.length }}</span>
<span :class="['badge', releasePrecheck.ok ? 'ok' : releasePrecheck.ranAt ? 'warn' : 'neutral']">
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
</span>
</div>
<div class="action-group">
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading || updateOps.running || restartOps.running">
{{ updateOps.loading || restartOps.loading ? '刷新中...' : '刷新目标' }}
</button>
<button class="refresh ghost" @click="runReleasePrecheck" :disabled="updateOps.running || restartOps.running">预检查</button>
<button class="refresh ghost" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
<button class="refresh danger" @click="runServiceUpdate" :disabled="updateOps.running || updateOps.selectedServices.length === 0">
{{ updateOps.running ? '更新中...' : `执行更新 ${updateOps.selectedServices.length}` }}
</button>
<button class="refresh danger" @click="runRollingRestart" :disabled="restartOps.running || restartOps.selectedServices.length === 0">
{{ restartOps.running ? '重启中...' : `执行重启 ${restartOps.selectedServices.length}` }}
</button>
</div>
</section>
</template> </template>
<div v-if="logViewer.open" class="log-viewer-overlay" @click.self="closeLogViewer"> <div v-if="logViewer.open" class="log-viewer-overlay" @click.self="closeLogViewer">

View File

@ -33,14 +33,14 @@
<strong>Yumi</strong> <strong>Yumi</strong>
<span>监控 / 配置 / 发布</span> <span>监控 / 配置 / 发布</span>
</a> </a>
<a class="entry-card" href="/common/">
<strong>通用</strong>
<span>MySQL / CDN 刷新</span>
</a>
<a class="entry-card" href="/haiyi/"> <a class="entry-card" href="/haiyi/">
<strong>HaiYi</strong> <strong>HaiYi</strong>
<span>入口已预留</span> <span>入口已预留</span>
</a> </a>
<a class="entry-card" href="/common/">
<strong>通用</strong>
<span>MySQL / CDN 刷新</span>
</a>
</div> </div>
<div class="entry-actions"> <div class="entry-actions">

View File

@ -32,6 +32,14 @@ class GatewayMysqlAliasTests(unittest.TestCase):
self.assertEqual(rewritten, "/yumi/api/cdn/overview?limit=20") self.assertEqual(rewritten, "/yumi/api/cdn/overview?limit=20")
def test_match_proxy_target_returns_haiyi_entry(self) -> None:
target = match_proxy_target("/haiyi/api/health")
self.assertIsNotNone(target)
self.assertEqual(target["app"], "haiyi")
self.assertEqual(target["basePath"], "/haiyi")
self.assertEqual(target["upstreamBasePath"], "/haiyi")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -4,7 +4,7 @@ import unittest
from unittest.mock import patch from unittest.mock import patch
from core.config import HARBOR_PROJECT, HARBOR_REGISTRY from core.config import HARBOR_PROJECT, HARBOR_REGISTRY
from monitors.yumi.service_release import build_update_targets_payload from monitors.yumi.service_release import build_update_targets_payload, deploy_updated_services_payload
class ServiceReleaseTargetsTests(unittest.TestCase): class ServiceReleaseTargetsTests(unittest.TestCase):
@ -163,6 +163,83 @@ class ServiceReleaseTargetsTests(unittest.TestCase):
self.assertEqual(len(payload["items"]), 1) self.assertEqual(len(payload["items"]), 1)
self.assertEqual(payload["items"][0]["repository"], f"{HARBOR_REGISTRY}/{HARBOR_PROJECT}/java/chatapp3-auth") self.assertEqual(payload["items"][0]["repository"], f"{HARBOR_REGISTRY}/{HARBOR_PROJECT}/java/chatapp3-auth")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_INSTANCE_ID", "ins-admin")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_IP", "10.2.1.2")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_HOST", "admin-platform")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_REPO_URL", "git@example.com:hy/chatapp3-admin.git")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_SITE_DIR", "/opt/yumi-admin/frontend-admin/site")
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=True)
@patch(
"monitors.yumi.service_release.frontend_release_display",
return_value={
"currentLabel": "main@abc123",
"currentTitle": "abc123",
"detailLabel": "abc123",
"detailTitle": "abc123",
"copyValue": "abc123",
},
)
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["admin"])
@patch("monitors.yumi.service_release.service_records_by_name", return_value={})
def test_build_update_targets_payload_adds_admin_frontend_target(
self,
service_records_by_name_mock,
buildable_service_names_mock,
frontend_release_display_mock,
frontend_release_enabled_mock,
) -> None:
del service_records_by_name_mock, buildable_service_names_mock
del frontend_release_display_mock, frontend_release_enabled_mock
payload = build_update_targets_payload()
self.assertTrue(payload["ok"])
self.assertEqual(len(payload["items"]), 1)
admin = payload["items"][0]
self.assertEqual(admin["service"], "admin")
self.assertEqual(admin["kind"], "frontend")
self.assertEqual(admin["repository"], "git@example.com:hy/chatapp3-admin.git")
self.assertEqual(admin["hosts"][0]["host"], "admin-platform")
self.assertEqual(admin["hosts"][0]["ip"], "10.2.1.2")
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=True)
@patch("monitors.yumi.service_release.service_records_by_name", return_value={})
@patch(
"monitors.yumi.service_release.deploy_frontend_site",
return_value={
"ok": True,
"updatedAt": "2026-04-24T00:00:00+00:00",
"services": ["admin"],
"build": [{"kind": "frontend", "gitRef": "release/admin", "commit": "abc123"}],
"templates": [],
"preparedHosts": [],
"steps": [{"service": "admin", "host": "admin-platform", "status": "success"}],
"message": "service update complete",
},
)
def test_deploy_updated_services_payload_dispatches_admin_frontend(
self,
deploy_frontend_site_mock,
service_records_by_name_mock,
frontend_release_enabled_mock,
) -> None:
del service_records_by_name_mock, frontend_release_enabled_mock
payload = deploy_updated_services_payload(
["admin"],
java_git_ref="main",
go_git_ref="main",
admin_git_ref="release/admin",
image_tag="",
skip_maven_build=False,
)
deploy_frontend_site_mock.assert_called_once_with("admin", git_ref="release/admin", progress=None)
self.assertTrue(payload["ok"])
self.assertEqual(payload["services"], ["admin"])
self.assertEqual(payload["build"][0]["kind"], "frontend")
self.assertEqual(payload["steps"][0]["service"], "admin")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()