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
JAVA_REPO_ROOT=/opt/deploy/sources/chatapp3-java
GOLANG_REPO_ROOT=/opt/deploy/sources/chatapp3-golang
ADMIN_FRONTEND_ENABLED=0
ADMIN_FRONTEND_ENABLED=1
ADMIN_FRONTEND_SERVICE_NAME=admin
ADMIN_FRONTEND_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-admin.git
ADMIN_FRONTEND_REPO_ROOT=/opt/deploy/sources/chatapp3-admin
ADMIN_FRONTEND_HOST=
ADMIN_FRONTEND_IP=
ADMIN_FRONTEND_INSTANCE_ID=
ADMIN_FRONTEND_SSH_HOST=
ADMIN_FRONTEND_HOST=admin-platform
ADMIN_FRONTEND_IP=10.2.1.2
ADMIN_FRONTEND_INSTANCE_ID=ins-p11begwe
ADMIN_FRONTEND_SSH_HOST=10.2.1.2
ADMIN_FRONTEND_SSH_USER=root
ADMIN_FRONTEND_SSH_PORT=22
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_BUILD_SCRIPT=build:antdv-next
ADMIN_FRONTEND_DIST_DIR=apps/dist
ADMIN_FRONTEND_PUBLIC_URL=
ADMIN_FRONTEND_PUBLIC_URL=https://yumi-admin.haiyihy.com
MONGO_URI=
MONGO_DB_NAME=tarab_all
MONGO_INSTANCE_ID=ins-o9mmob66

View File

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

View File

@ -6,6 +6,11 @@ const APP_CONTEXTS = [
{ id: "common", basePath: "/common", 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 UPDATE_OPERATION_STORAGE_KEY = "hy-app-monitor-service-update-operation";
const GROUP_LABELS = {
@ -402,6 +407,8 @@ createApp({
const isCommonMode = CURRENT_APP_CONTEXT.mode === "common";
return {
activeView: isCommonMode ? "common" : "overview",
appSwitchLinks: APP_SWITCH_LINKS,
currentAppEntryId: CURRENT_APP_CONTEXT.id === "mysql" ? "common" : CURRENT_APP_CONTEXT.id,
databaseEngine: isCommonMode ? "mysql" : "mongo",
commonTool: "mysql",
densityMode: "compact",
@ -1727,47 +1734,14 @@ createApp({
goConfigDirty() {
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() {
const keyword = this.normalizedReleaseKeyword;
return (this.updateOps.items || [])
.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(),
};
})
return this.allReleaseTargetRows
.filter((item) => {
if (this.releaseKindFilter !== "all" && item.kind !== this.releaseKindFilter) {
return false;
@ -1776,8 +1750,7 @@ createApp({
return false;
}
return true;
})
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
});
},
restartTargetRows() {
const keyword = this.normalizedReleaseKeyword;
@ -1816,13 +1789,13 @@ createApp({
},
{
label: "最近结果",
value: this.updateStatusLabel(),
tone: this.updateStatusBadgeClass(),
value: this.releasePrimaryStatusLabel(),
tone: this.releasePrimaryStatusTone(),
},
{
label: "失败阶段",
value: this.updateOps.error ? (this.updateOps.stage || "failed") : "-",
tone: this.updateOps.error ? "bad" : "neutral",
value: this.updateOps.error || this.restartOps.error ? (this.updateOps.stage || "failed") : "-",
tone: this.updateOps.error || this.restartOps.error ? "bad" : "neutral",
},
{
label: "影响服务",
@ -2446,6 +2419,44 @@ createApp({
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) {
const text = String(value || "");
if (!text) {
@ -3258,6 +3269,83 @@ createApp({
}
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() {
const settings = this.payload.settings || {};
const options = Array.isArray(settings.refreshIntervalOptions)
@ -5974,11 +6062,14 @@ createApp({
}
this.updateOps.selectedServices = [...next];
},
async runServiceUpdate() {
if (this.updateOps.selectedServices.length === 0) {
async runServiceUpdate(serviceNames = this.updateOps.selectedServices) {
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;
}
this.updateOps.selectedServices = services;
this.stopUpdatePolling();
this.updateOps.requestEpoch += 1;
this.updateOps.statusRequestInFlight = false;
@ -5995,7 +6086,7 @@ createApp({
this.updateOps.logs = [];
this.updateOps.result = null;
this.persistUpdateOperationId();
this.recordReleaseHistory("update", "提交服务更新", this.updateOps.selectedServices.join(", "), "warn");
this.recordReleaseHistory("update", "提交服务更新", services.join(", "), "warn");
try {
const { response, payload } = await this.requestJson("/api/ops/update-services", {
@ -6004,7 +6095,7 @@ createApp({
"Content-Type": "application/json",
},
body: JSON.stringify({
services: this.updateOps.selectedServices,
services,
javaGitRef: this.updateOps.javaGitRef,
goGitRef: this.updateOps.goGitRef,
adminGitRef: this.updateOps.adminGitRef,

View File

@ -37,7 +37,9 @@
</div>
<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-logout" class="refresh ghost" type="button">退出</button>
</div>

View File

@ -24,6 +24,16 @@
<span class="env-pill">DEPLOY</span>
</div>
<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>
<nav v-if="viewTabs.length > 1" class="topbar-nav" aria-label="主导航">
@ -41,10 +51,9 @@
<span :class="['badge', globalHealthTone]">{{ globalHealthLabel }}</span>
<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 :class="['badge', loading ? 'warn' : 'neutral']">{{ loading ? '刷新中' : '在线' }}</span>
<span v-if="loading" class="badge warn">刷新中</span>
<span v-if="error" class="badge bad">接口异常</span>
<label class="interval-field topbar-interval">
<span>刷新</span>
<label class="interval-field topbar-interval" aria-label="刷新间隔">
<select v-model.number="refreshIntervalSeconds" class="interval-select">
<option v-for="seconds in refreshIntervalOptions" :key="seconds" :value="seconds">{{ seconds }}s</option>
</select>
@ -304,12 +313,13 @@
<div class="control-group release-section-nav">
<button
v-for="section in releaseSections"
v-for="(section, index) in releaseSections"
:key="section.id"
class="section-link"
:class="['section-link', 'release-step-link', `tone-${releaseStepTone(section.id)}`]"
@click="scrollToSection(section.id)"
>
{{ section.label }}
<span class="release-step-index">{{ index + 1 }}</span>
<span>{{ section.label }}</span>
</button>
</div>
@ -2980,15 +2990,14 @@
{{ nacosConfig.saving ? '保存中...' : '保存' }}
</button>
</div>
</div>
<div class="metadata-strip metadata-strip-tight config-inline-strip">
<div class="editor-toolbar-meta">
<span>最近修改 {{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</span>
<span>{{ nacosConfig.validationMessage ? '已校验' : '未校验' }}</span>
<span v-if="nacosConfig.editor.appName">app {{ nacosConfig.editor.appName }}</span>
<span v-if="nacosConfig.editor.description">{{ nacosConfig.editor.description }}</span>
<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">
<div v-if="showNacosDiffEmpty" class="config-empty">当前没有变更,切到源码继续编辑</div>
@ -3132,16 +3141,47 @@
</div>
</div>
<div class="release-action-summary">
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
<span class="badge neutral">重启目标 {{ restartTargetRows.length }}</span>
<span class="badge neutral">已选更新 {{ updateOps.selectedServices.length }}</span>
<span class="badge neutral">已选重启 {{ restartOps.selectedServices.length }}</span>
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']">
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
<div class="release-command-row" aria-label="发布操作">
<div class="release-action-summary compact">
<span :class="['badge', releasePrimaryStatusTone()]">{{ releasePrimaryStatusLabel() }}</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']">
{{ releasePrecheckBadgeLabel() }}
</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 class="metadata-strip metadata-strip-tight release-meta-strip">
<span
@ -3155,7 +3195,7 @@
</div>
<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
v-for="check in releasePrecheck.checks"
@ -3168,7 +3208,7 @@
</div>
<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 id="release-build" class="panel infra-panel">
@ -3258,6 +3298,17 @@
</span>
<small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small>
<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
v-if="item.copyValue"
class="refresh ghost inline-button"
@ -3478,29 +3529,6 @@
</details>
</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>
<div v-if="logViewer.open" class="log-viewer-overlay" @click.self="closeLogViewer">

View File

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

View File

@ -32,6 +32,14 @@ class GatewayMysqlAliasTests(unittest.TestCase):
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__":
unittest.main()

View File

@ -4,7 +4,7 @@ import unittest
from unittest.mock import patch
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):
@ -163,6 +163,83 @@ class ServiceReleaseTargetsTests(unittest.TestCase):
self.assertEqual(len(payload["items"]), 1)
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__":
unittest.main()