chore: test deploy snapshot

This commit is contained in:
2026-05-27 10:43:01 +09:00
parent c1d0f4c1db
commit 4c4b3c8d2c
78 changed files with 10392 additions and 2301 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,14 @@ import type {
ServerRestartReservation,
ServerRestartReservationStatus,
ServerRestartReservationWorkItem,
TestServerDeploymentPhase,
TestServerDeploymentState,
TestServerDeploymentStep,
TestServerDeploymentStepKey,
WorkServerDeploymentPhase,
WorkServerDeploymentState,
WorkServerDeploymentStep,
WorkServerDeploymentStepKey,
} from './types';
export class ServerCommandApiError extends Error {
@@ -259,6 +267,7 @@ function normalizeServerCommandItem(value: unknown): ServerCommandItem {
commandScript: typeof item.commandScript === 'string' ? item.commandScript : '-',
commandWorkingDirectory: typeof item.commandWorkingDirectory === 'string' ? item.commandWorkingDirectory : '-',
errorMessage: typeof item.errorMessage === 'string' ? item.errorMessage : null,
deployment: normalizeWorkServerDeploymentState(item.deployment),
};
}
@@ -343,12 +352,14 @@ function extractServerCommandActionResult(response: unknown): ServerCommandActio
commandOutput?: unknown;
output?: unknown;
restartState?: unknown;
testDeployment?: unknown;
data?: {
item?: unknown;
server?: unknown;
commandOutput?: unknown;
output?: unknown;
restartState?: unknown;
testDeployment?: unknown;
};
};
const nestedData = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : null;
@@ -365,9 +376,235 @@ function extractServerCommandActionResult(response: unknown): ServerCommandActio
item: normalizeServerCommandItem(item),
commandOutput: typeof commandOutput === 'string' ? commandOutput : null,
restartState: restartState === 'accepted' ? 'accepted' : 'completed',
deployment: normalizeWorkServerDeploymentState(payload.deployment ?? nestedData?.deployment),
testDeployment: normalizeTestServerDeploymentState(payload.testDeployment ?? nestedData?.testDeployment),
};
}
const WORK_SERVER_DEPLOYMENT_STEP_KEYS = [
'build-target-slot',
'verify-target-health',
'switch-proxy',
'drain-previous-slot',
'rebuild-previous-slot',
'recover-interrupted-chat',
] as const;
function normalizeWorkServerDeploymentStepKey(value: unknown): WorkServerDeploymentStepKey | null {
return WORK_SERVER_DEPLOYMENT_STEP_KEYS.includes(value as WorkServerDeploymentStepKey)
? (value as WorkServerDeploymentStepKey)
: null;
}
function normalizeWorkServerDeploymentPhase(value: unknown): WorkServerDeploymentPhase {
return value === 'build-target-slot'
|| value === 'verify-target-health'
|| value === 'switch-proxy'
|| value === 'drain-previous-slot'
|| value === 'rebuild-previous-slot'
|| value === 'recover-interrupted-chat'
|| value === 'completed'
|| value === 'failed'
? value
: 'idle';
}
function normalizeWorkServerDeploymentSteps(value: unknown): WorkServerDeploymentStep[] {
if (!Array.isArray(value)) {
return WORK_SERVER_DEPLOYMENT_STEP_KEYS.map((key) => ({
key,
status: 'pending',
detail: null,
updatedAt: null,
}));
}
const normalizedByKey = new Map<WorkServerDeploymentStepKey, WorkServerDeploymentStep>();
value.forEach((item) => {
if (!item || typeof item !== 'object') {
return;
}
const candidate = item as Record<string, unknown>;
const key = normalizeWorkServerDeploymentStepKey(candidate.key);
if (!key) {
return;
}
normalizedByKey.set(key, {
key,
status:
candidate.status === 'running'
|| candidate.status === 'completed'
|| candidate.status === 'failed'
|| candidate.status === 'pending'
? candidate.status
: 'pending',
detail: typeof candidate.detail === 'string' ? candidate.detail : null,
updatedAt: typeof candidate.updatedAt === 'string' ? candidate.updatedAt : null,
});
});
return WORK_SERVER_DEPLOYMENT_STEP_KEYS.map((key) => normalizedByKey.get(key) ?? {
key,
status: 'pending',
detail: null,
updatedAt: null,
});
}
function normalizeWorkServerDeploymentState(value: unknown): WorkServerDeploymentState | null {
if (!value || typeof value !== 'object') {
return null;
}
const item = value as Record<string, unknown>;
return {
status: item.status === 'running' || item.status === 'completed' || item.status === 'failed' ? item.status : 'idle',
phase: normalizeWorkServerDeploymentPhase(item.phase),
summary: typeof item.summary === 'string' ? item.summary : null,
startedAt: typeof item.startedAt === 'string' ? item.startedAt : null,
updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,
completedAt: typeof item.completedAt === 'string' ? item.completedAt : null,
activeSlot: item.activeSlot === 'blue' || item.activeSlot === 'green' ? item.activeSlot : null,
targetSlot: item.targetSlot === 'blue' || item.targetSlot === 'green' ? item.targetSlot : null,
previousSlot: item.previousSlot === 'blue' || item.previousSlot === 'green' ? item.previousSlot : null,
targetContainer: typeof item.targetContainer === 'string' ? item.targetContainer : null,
previousContainer: typeof item.previousContainer === 'string' ? item.previousContainer : null,
previousSlotActiveChatRequestCount: typeof item.previousSlotActiveChatRequestCount === 'number' ? item.previousSlotActiveChatRequestCount : null,
previousSlotQueuedChatRequestCount: typeof item.previousSlotQueuedChatRequestCount === 'number' ? item.previousSlotQueuedChatRequestCount : null,
recoveredSessionCount: typeof item.recoveredSessionCount === 'number' ? item.recoveredSessionCount : null,
recoveredRestartedCount: typeof item.recoveredRestartedCount === 'number' ? item.recoveredRestartedCount : null,
recoveredRequeuedCount: typeof item.recoveredRequeuedCount === 'number' ? item.recoveredRequeuedCount : null,
lastError: typeof item.lastError === 'string' ? item.lastError : null,
logExcerpt: typeof item.logExcerpt === 'string' ? item.logExcerpt : null,
steps: normalizeWorkServerDeploymentSteps(item.steps),
};
}
function extractWorkServerDeploymentState(response: unknown): WorkServerDeploymentState | null {
if (!response || typeof response !== 'object') {
return null;
}
const payload = response as {
item?: unknown;
data?: {
item?: unknown;
};
};
const nestedData = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : null;
return normalizeWorkServerDeploymentState(payload.item ?? nestedData?.item ?? null);
}
const TEST_SERVER_DEPLOYMENT_STEP_KEYS = [
'commit-main-worktree',
'push-origin-main',
'build-test-app',
'deploy-test-server',
] as const;
function normalizeTestServerDeploymentStepKey(value: unknown): TestServerDeploymentStepKey | null {
return TEST_SERVER_DEPLOYMENT_STEP_KEYS.includes(value as TestServerDeploymentStepKey)
? (value as TestServerDeploymentStepKey)
: null;
}
function normalizeTestServerDeploymentPhase(value: unknown): TestServerDeploymentPhase {
return value === 'commit-main-worktree'
|| value === 'push-origin-main'
|| value === 'build-test-app'
|| value === 'deploy-test-server'
|| value === 'completed'
|| value === 'failed'
? value
: 'idle';
}
function normalizeTestServerDeploymentSteps(value: unknown): TestServerDeploymentStep[] {
if (!Array.isArray(value)) {
return TEST_SERVER_DEPLOYMENT_STEP_KEYS.map((key) => ({
key,
status: 'pending',
detail: null,
updatedAt: null,
}));
}
const normalizedByKey = new Map<TestServerDeploymentStepKey, TestServerDeploymentStep>();
value.forEach((item) => {
if (!item || typeof item !== 'object') {
return;
}
const candidate = item as Record<string, unknown>;
const key = normalizeTestServerDeploymentStepKey(candidate.key);
if (!key) {
return;
}
normalizedByKey.set(key, {
key,
status:
candidate.status === 'running'
|| candidate.status === 'completed'
|| candidate.status === 'failed'
|| candidate.status === 'pending'
? candidate.status
: 'pending',
detail: typeof candidate.detail === 'string' ? candidate.detail : null,
updatedAt: typeof candidate.updatedAt === 'string' ? candidate.updatedAt : null,
});
});
return TEST_SERVER_DEPLOYMENT_STEP_KEYS.map((key) => normalizedByKey.get(key) ?? {
key,
status: 'pending',
detail: null,
updatedAt: null,
});
}
function normalizeTestServerDeploymentState(value: unknown): TestServerDeploymentState | null {
if (!value || typeof value !== 'object') {
return null;
}
const item = value as Record<string, unknown>;
return {
status: item.status === 'running' || item.status === 'completed' || item.status === 'failed' ? item.status : 'idle',
phase: normalizeTestServerDeploymentPhase(item.phase),
summary: typeof item.summary === 'string' ? item.summary : null,
startedAt: typeof item.startedAt === 'string' ? item.startedAt : null,
updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,
completedAt: typeof item.completedAt === 'string' ? item.completedAt : null,
lastError: typeof item.lastError === 'string' ? item.lastError : null,
logExcerpt: typeof item.logExcerpt === 'string' ? item.logExcerpt : null,
steps: normalizeTestServerDeploymentSteps(item.steps),
};
}
function extractTestServerDeploymentState(response: unknown): TestServerDeploymentState | null {
if (!response || typeof response !== 'object') {
return null;
}
const payload = response as {
item?: unknown;
data?: {
item?: unknown;
};
};
const nestedData = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : null;
return normalizeTestServerDeploymentState(payload.item ?? nestedData?.item ?? null);
}
function normalizeServerRestartReservationStatus(value: unknown): ServerRestartReservationStatus {
return value === 'waiting'
|| value === 'ready'
@@ -538,6 +775,42 @@ export async function restartServerCommand(key: ServerCommandKey, options?: { si
return extractServerCommandActionResult(response);
}
export async function deployWorkServerCommand(options?: { signal?: AbortSignal; shareToken?: string | null }) {
const response = await request<unknown>('/server-commands/work-server/actions/deploy', {
method: 'POST',
body: JSON.stringify({}),
signal: options?.signal,
timeoutMs: 12000,
}, { shareToken: options?.shareToken });
return extractServerCommandActionResult(response);
}
export async function deployTestServerCommand(options?: { signal?: AbortSignal; shareToken?: string | null }) {
const response = await request<unknown>('/server-commands/test/actions/deploy', {
method: 'POST',
body: JSON.stringify({}),
signal: options?.signal,
timeoutMs: 120000,
}, { shareToken: options?.shareToken });
return extractServerCommandActionResult(response);
}
export async function fetchWorkServerDeploymentStatus(options?: { signal?: AbortSignal; shareToken?: string | null }) {
const response = await request<unknown>('/server-commands/work-server/deployment', {
signal: options?.signal,
}, { shareToken: options?.shareToken });
return extractWorkServerDeploymentState(response);
}
export async function fetchTestServerDeploymentStatus(options?: { signal?: AbortSignal; shareToken?: string | null }) {
const response = await request<unknown>('/server-commands/test/deployment', {
signal: options?.signal,
}, { shareToken: options?.shareToken });
return extractTestServerDeploymentState(response);
}
export async function fetchServerRestartReservation(options?: { signal?: AbortSignal; shareToken?: string | null }) {
const response = await request<unknown>('/server-commands/restart-reservation', {
signal: options?.signal,

View File

@@ -5,8 +5,12 @@
display: flex;
flex-direction: column;
overflow: auto;
overscroll-behavior: contain;
overscroll-behavior: none;
overscroll-behavior-y: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
padding: 0 0 calc(16px + env(safe-area-inset-bottom, 0px));
background: #f3f6fb;
}
.server-command-page.ant-space,
@@ -22,77 +26,37 @@
width: 100%;
}
.server-command-page__alert-body {
.server-command-page__surface {
width: 100%;
padding: 12px 14px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #ffffff;
box-shadow: none;
}
.server-command-page__alert-code {
display: block;
width: 100%;
padding: 10px 12px;
border-radius: 12px;
background: #fff2f0;
white-space: pre-wrap;
word-break: break-word;
user-select: text;
-webkit-user-select: text;
.server-command-page__surface--toolbar {
background: #ffffff;
}
.server-command-page__card,
.server-command-page__server-card {
border-radius: 24px;
.server-command-page__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.server-command-page__reservation-card {
border: 1px solid #d6e4ff;
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%);
}
.server-command-page__card--shared {
background:
linear-gradient(135deg, rgba(15, 23, 42, 0.96) 0%, rgba(30, 41, 59, 0.94) 52%, rgba(37, 99, 235, 0.88) 100%);
color: #e2e8f0;
}
.server-command-page__card--shared .server-command-page__title.ant-typography,
.server-command-page__card--shared .server-command-page__copy.ant-typography {
color: inherit;
}
.server-command-page__server-card {
.server-command-page__toolbar-copy {
min-width: 0;
}
.server-command-page__server-card--shared {
border: 1px solid rgba(148, 163, 184, 0.2);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.96) 100%);
box-shadow:
0 20px 44px rgba(15, 23, 42, 0.12),
inset 0 0 0 1px rgba(255, 255, 255, 0.75);
}
.server-command-page__server-card--shared-compact {
border-radius: 20px;
box-shadow:
0 12px 28px rgba(15, 23, 42, 0.08),
inset 0 0 0 1px rgba(255, 255, 255, 0.72);
}
.server-command-page__server-card--online {
border-color: rgba(147, 197, 253, 0.72);
}
.server-command-page__server-card--degraded {
border-color: rgba(253, 230, 138, 0.92);
background:
linear-gradient(180deg, rgba(255, 251, 235, 0.95) 0%, rgba(255, 255, 255, 0.98) 100%);
}
.server-command-page__server-card--offline {
border-color: rgba(254, 202, 202, 0.9);
background:
linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 255, 255, 0.98) 100%);
.server-command-page__toolbar-side {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
min-width: 0;
flex-wrap: wrap;
}
.server-command-page__title.ant-typography,
@@ -100,300 +64,267 @@
margin-bottom: 0;
}
.server-command-page__title-row {
align-items: center;
}
.server-command-page__title-row .ant-space-item {
display: inline-flex;
align-items: center;
}
.server-command-page__copy.ant-typography {
max-width: 760px;
margin-bottom: 0;
}
.server-command-page__shared-toolbar {
display: flex;
justify-content: space-between;
gap: 10px;
}
.server-command-page__shared-toolbar-chips {
min-width: 0;
}
.server-command-page__toolbar-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
color: #e2e8f0;
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.server-command-page__toolbar-chip-label {
color: rgba(226, 232, 240, 0.72);
letter-spacing: 0.06em;
}
.server-command-page__toolbar-chip-value {
min-width: 0;
}
.server-command-page__toolbar-chip--online,
.server-command-page__toolbar-chip--latest {
background: rgba(37, 99, 235, 0.2);
color: #bfdbfe;
}
.server-command-page__toolbar-chip--degraded,
.server-command-page__toolbar-chip--update-available {
background: rgba(245, 158, 11, 0.2);
color: #fde68a;
}
.server-command-page__toolbar-chip--offline,
.server-command-page__toolbar-chip--build-required {
background: rgba(220, 38, 38, 0.2);
color: #fecaca;
}
.server-command-page__toolbar-chip--unknown,
.server-command-page__toolbar-chip--info {
background: rgba(148, 163, 184, 0.18);
color: #e2e8f0;
}
.server-command-page__status-dot {
width: 10px;
height: 10px;
flex: 0 0 auto;
border-radius: 999px;
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.14);
}
.server-command-page__status-dot--online,
.server-command-page__status-dot--latest {
background: #2563eb;
}
.server-command-page__status-dot--degraded,
.server-command-page__status-dot--update-available {
background: #f59e0b;
}
.server-command-page__status-dot--offline,
.server-command-page__status-dot--build-required {
background: #dc2626;
}
.server-command-page__status-dot--unknown {
background: #94a3b8;
}
.server-command-page__summary-grid {
width: 100%;
}
.server-command-page__shared-server-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
gap: 12px;
}
.server-command-page__shared-server-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.server-command-page__shared-server-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.server-command-page__shared-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
min-height: 32px;
padding: 0 9px;
border-radius: 999px;
background: rgba(226, 232, 240, 0.7);
color: #334155;
font-size: 14px;
font-weight: 600;
line-height: 1;
}
.server-command-page__shared-pill--online,
.server-command-page__shared-pill--latest {
background: rgba(219, 234, 254, 0.95);
color: #1d4ed8;
}
.server-command-page__shared-pill--degraded,
.server-command-page__shared-pill--update-available {
background: rgba(254, 240, 138, 0.55);
color: #b45309;
}
.server-command-page__shared-pill--offline,
.server-command-page__shared-pill--build-required {
background: rgba(254, 226, 226, 0.92);
color: #b91c1c;
}
.server-command-page__shared-server-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.server-command-page__shared-stat {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border-radius: 14px;
background: rgba(248, 250, 252, 0.92);
}
.server-command-page__shared-server-summary.ant-typography,
.server-command-page__shared-server-footer.ant-typography {
margin-bottom: 0;
}
.server-command-page__shared-server-summary.ant-typography {
font-size: 12px;
}
.server-command-page__restart-button--shared-compact {
min-width: 88px;
height: 36px;
border-radius: 12px;
font-weight: 700;
}
.server-command-page__summary-grid .ant-statistic {
padding: 14px 16px;
border-radius: 18px;
background: #f7faff;
}
.server-command-page__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
gap: 16px;
}
.server-command-page__copy.ant-typography,
.server-command-page__summary.ant-typography,
.server-command-page__preview.ant-typography,
.server-command-page__shared-server-footer.ant-typography,
.server-command-page__work-detail.ant-typography,
.server-command-page__command.ant-typography {
margin-bottom: 0;
}
.server-command-page__copy.ant-typography {
margin-top: 2px;
color: #6b7280;
}
.server-command-page__status-summary {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
flex-wrap: wrap;
}
.server-command-page__status-summary-item {
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 28px;
padding: 0 9px;
border: 1px solid #e5e7eb;
border-radius: 999px;
background: #f8fafc;
color: #475569;
font-size: 12px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.server-command-page__status-summary-item strong {
font-size: 12px;
font-weight: 700;
}
.server-command-page__status-summary-item--online {
color: #166534;
background: #f0fdf4;
border-color: #bbf7d0;
}
.server-command-page__status-summary-item--latest {
color: #1d4ed8;
background: #eff6ff;
border-color: #bfdbfe;
}
.server-command-page__status-summary-item--degraded,
.server-command-page__status-summary-item--update-available {
color: #b45309;
background: #fffbeb;
border-color: #fde68a;
}
.server-command-page__status-summary-item--offline,
.server-command-page__status-summary-item--build-required {
color: #b91c1c;
background: #fef2f2;
border-color: #fecaca;
}
.server-command-page__status-summary-item--unknown,
.server-command-page__status-summary-item--info {
color: #475569;
}
.server-command-page__reservation-panel {
border-color: #dbeafe;
background: #fcfdff;
}
.server-command-page__alert-body {
width: 100%;
}
.server-command-page__alert-code,
.server-command-page__preview,
.server-command-page__command {
.server-command-page__work-item {
display: block;
padding: 12px 14px;
border-radius: 16px;
background: #f7faff;
width: 100%;
padding: 8px 10px;
border-radius: 8px;
background: #f8fafc;
white-space: pre-wrap;
word-break: break-word;
}
.server-command-page__preview-block {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.server-command-page__preview-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
}
.server-command-page__alert-code {
background: #fff2f0;
}
.server-command-page__alert-text {
display: block;
white-space: pre-wrap;
word-break: break-word;
user-select: text;
-webkit-user-select: text;
-webkit-touch-callout: default;
}
.server-command-page__compact-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.server-command-page__compact-item {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border: 1px solid #eef2f7;
border-radius: 12px;
background: #fbfcfe;
}
.server-command-page__restart-button {
min-width: 96px;
height: 32px;
border-radius: 8px;
font-weight: 600;
}
.server-command-page__work-list {
width: 100%;
}
.server-command-page__work-item {
width: 100%;
padding: 12px 14px;
border-radius: 16px;
background: #f7faff;
.server-command-page__control-list {
display: grid;
gap: 12px;
}
.server-command-page__work-detail.ant-typography {
.server-command-page__control-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
border-color: #d7deea;
}
.server-command-page__control-main {
min-width: 0;
flex: 1 1 auto;
}
.server-command-page__control-header {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
margin-bottom: 10px;
}
.server-command-page__control-title.ant-typography {
margin-bottom: 0;
font-size: 15px;
}
.server-command-page__status-badge {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid #cbd5e1;
background: #f8fafc;
color: #334155;
font-size: 12px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.server-command-page__status-badge--online,
.server-command-page__status-badge--latest {
border-color: #93c5fd;
background: #dbeafe;
color: #1d4ed8;
}
.server-command-page__status-badge--degraded,
.server-command-page__status-badge--update-available {
border-color: #fcd34d;
background: #fef3c7;
color: #b45309;
}
.server-command-page__status-badge--offline,
.server-command-page__status-badge--build-required {
border-color: #fca5a5;
background: #fee2e2;
color: #b91c1c;
}
.server-command-page__status-badge--unknown,
.server-command-page__status-badge--info {
border-color: #cbd5e1;
background: #f8fafc;
color: #475569;
}
.server-command-page__control-meta {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 8px;
}
.server-command-page__control-meta + .server-command-page__control-meta {
margin-top: 4px;
}
.server-command-page__control-meta .ant-typography {
margin-bottom: 0;
}
.server-command-page__meta .ant-descriptions-item-label {
width: 104px;
}
.server-command-page__restart-button--shared {
min-width: 180px;
height: 44px;
border-radius: 14px;
font-weight: 700;
.server-command-page__control-button {
flex: 0 0 auto;
}
@media (max-width: 768px) {
.server-command-page__shared-server-head {
align-items: stretch;
flex-direction: column;
}
.server-command-page__shared-server-stats {
grid-template-columns: 1fr;
}
.server-command-page__shared-toolbar {
.server-command-page__toolbar {
flex-direction: column;
align-items: stretch;
}
.server-command-page__restart-button--shared {
width: 100%;
}
.server-command-page__restart-button--shared-compact {
width: 100%;
}
.server-command-page__server-card .ant-card-head {
padding-inline: 16px;
}
.server-command-page__server-card .ant-card-head-wrapper {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.server-command-page__server-card .ant-card-extra {
margin-inline-start: 0;
.server-command-page__toolbar-side {
justify-content: space-between;
}
.server-command-page__restart-button {
width: 100%;
min-width: 64px;
}
.server-command-page__server-card .ant-card-body {
padding-inline: 16px;
.server-command-page__control-card {
align-items: flex-start;
}
.server-command-page__grid {
grid-template-columns: 1fr;
.server-command-page__control-button {
min-width: 72px;
}
}

View File

@@ -38,12 +38,97 @@ export type ServerCommandItem = {
commandScript: string;
commandWorkingDirectory: string;
errorMessage: string | null;
deployment: WorkServerDeploymentState | null;
};
export type ServerCommandActionResult = {
item: ServerCommandItem;
commandOutput: string | null;
restartState: 'completed' | 'accepted';
deployment?: WorkServerDeploymentState | null;
testDeployment?: TestServerDeploymentState | null;
};
export type WorkServerDeploymentStepKey =
| 'build-target-slot'
| 'verify-target-health'
| 'switch-proxy'
| 'drain-previous-slot'
| 'rebuild-previous-slot'
| 'recover-interrupted-chat';
export type WorkServerDeploymentStepStatus = 'pending' | 'running' | 'completed' | 'failed';
export type WorkServerDeploymentStep = {
key: WorkServerDeploymentStepKey;
status: WorkServerDeploymentStepStatus;
detail: string | null;
updatedAt: string | null;
};
export type WorkServerDeploymentPhase =
| 'idle'
| 'build-target-slot'
| 'verify-target-health'
| 'switch-proxy'
| 'drain-previous-slot'
| 'rebuild-previous-slot'
| 'recover-interrupted-chat'
| 'completed'
| 'failed';
export type WorkServerDeploymentState = {
status: 'idle' | 'running' | 'completed' | 'failed';
phase: WorkServerDeploymentPhase;
summary: string | null;
startedAt: string | null;
updatedAt: string | null;
completedAt: string | null;
activeSlot: 'blue' | 'green' | null;
targetSlot: 'blue' | 'green' | null;
previousSlot: 'blue' | 'green' | null;
targetContainer: string | null;
previousContainer: string | null;
previousSlotActiveChatRequestCount: number | null;
previousSlotQueuedChatRequestCount: number | null;
recoveredSessionCount: number | null;
recoveredRestartedCount: number | null;
recoveredRequeuedCount: number | null;
lastError: string | null;
logExcerpt: string | null;
steps: WorkServerDeploymentStep[];
};
export type TestServerDeploymentStepKey = 'commit-main-worktree' | 'push-origin-main' | 'build-test-app' | 'deploy-test-server';
export type TestServerDeploymentStepStatus = 'pending' | 'running' | 'completed' | 'failed';
export type TestServerDeploymentStep = {
key: TestServerDeploymentStepKey;
status: TestServerDeploymentStepStatus;
detail: string | null;
updatedAt: string | null;
};
export type TestServerDeploymentPhase =
| 'idle'
| 'commit-main-worktree'
| 'push-origin-main'
| 'build-test-app'
| 'deploy-test-server'
| 'completed'
| 'failed';
export type TestServerDeploymentState = {
status: 'idle' | 'running' | 'completed' | 'failed';
phase: TestServerDeploymentPhase;
summary: string | null;
startedAt: string | null;
updatedAt: string | null;
completedAt: string | null;
lastError: string | null;
logExcerpt: string | null;
steps: TestServerDeploymentStep[];
};
export type ServerRestartReservationStatus =