feat: update codex live runtime and restart flow

This commit is contained in:
2026-04-23 18:10:43 +09:00
parent b0b9980a6c
commit 6e863feafd
36 changed files with 1636 additions and 358 deletions

View File

@@ -15,6 +15,7 @@ import {
Alert,
Button,
Checkbox,
Divider,
Drawer,
Dropdown,
Grid,
@@ -869,6 +870,7 @@ export function MainHeader({
void contentExpanded;
void onToggleContentExpanded;
const screens = useBreakpoint();
const [modalApi, modalContextHolder] = Modal.useModal();
const navigate = useNavigate();
const location = useLocation();
const [settingsOpen, setSettingsOpen] = useState(false);
@@ -911,9 +913,12 @@ export function MainHeader({
const [clientResetFeedback, setClientResetFeedback] = useState<InlineFeedback | null>(null);
const [clientResetCopyFeedback, setClientResetCopyFeedback] = useState<InlineFeedback | null>(null);
const [testServerStatus, setTestServerStatus] = useState<ServerCommandItem | null>(null);
const [prodServerStatus, setProdServerStatus] = useState<ServerCommandItem | null>(null);
const [workServerStatus, setWorkServerStatus] = useState<ServerCommandItem | null>(null);
const [workServerStatusLoading, setWorkServerStatusLoading] = useState(false);
const [serverRestartingKey, setServerRestartingKey] = useState<'test' | 'work-server' | 'all' | null>(null);
const [serverRestartingKey, setServerRestartingKey] = useState<
'test' | 'prod' | 'work-server' | 'command-runner' | 'all' | null
>(null);
const [serverRestartFeedback, setServerRestartFeedback] = useState<InlineFeedback | null>(null);
const [serverRestartCopyFeedback, setServerRestartCopyFeedback] = useState<InlineFeedback | null>(null);
const { registeredToken, hasAccess } = useTokenAccess();
@@ -941,9 +946,11 @@ export function MainHeader({
: 'app-header__status-dot--inactive';
const testServerPendingUpdateCount =
testServerStatus && (testServerStatus.updateAvailable || testServerStatus.buildRequired) ? 1 : 0;
const prodServerPendingUpdateCount =
prodServerStatus && (prodServerStatus.updateAvailable || prodServerStatus.buildRequired) ? 1 : 0;
const workServerPendingUpdateCount =
workServerStatus && (workServerStatus.updateAvailable || workServerStatus.buildRequired) ? 1 : 0;
const totalPendingUpdateCount = testServerPendingUpdateCount + workServerPendingUpdateCount;
const totalPendingUpdateCount = testServerPendingUpdateCount + prodServerPendingUpdateCount + workServerPendingUpdateCount;
const settingsStatusClassName =
totalPendingUpdateCount >= 2
? 'app-header__status-dot--inactive'
@@ -989,6 +996,8 @@ export function MainHeader({
const searchParams = new URLSearchParams(location.search);
searchParams.set('topMenu', 'chat');
searchParams.set('sessionId', sessionId);
searchParams.delete('chatView');
searchParams.delete('runtimeRequestId');
navigate({
pathname: buildChatPath('live'),
search: `?${searchParams.toString()}`,
@@ -1482,18 +1491,22 @@ export function MainHeader({
const refreshServerStatuses = async () => {
const items = await fetchServerCommands();
const nextTestServerStatus = items.find((item) => item.key === 'test') ?? null;
const nextProdServerStatus = items.find((item) => item.key === 'prod') ?? null;
const nextWorkServerStatus = items.find((item) => item.key === 'work-server') ?? null;
setTestServerStatus(nextTestServerStatus);
setProdServerStatus(nextProdServerStatus);
setWorkServerStatus(nextWorkServerStatus);
return {
test: nextTestServerStatus,
prod: nextProdServerStatus,
'work-server': nextWorkServerStatus,
} satisfies Record<'test' | 'work-server', ServerCommandItem | null>;
} satisfies Record<'test' | 'prod' | 'work-server', ServerCommandItem | null>;
};
const refreshUpdateTargets = async (silent = false) => {
if (!hasAccess) {
setTestServerStatus(null);
setProdServerStatus(null);
setWorkServerStatus(null);
if (!silent) {
setUpdateCheckFeedback({ tone: 'warning', message: '업데이트 확인은 권한 토큰 등록 후 사용할 수 있습니다.' });
@@ -1515,7 +1528,7 @@ export function MainHeader({
if (!silent) {
setUpdateCheckFeedback({
tone: 'error',
message: error instanceof Error ? error.message : 'TEST/WORK 서버 업데이트 상태를 불러오지 못했습니다.',
message: error instanceof Error ? error.message : 'TEST/PROD/WORK 서버 업데이트 상태를 불러오지 못했습니다.',
});
}
return null;
@@ -1524,7 +1537,7 @@ export function MainHeader({
}
};
const waitForServerRestart = async (key: 'test' | 'work-server', baseline: ServerCommandItem | null) => {
const waitForServerRestart = async (key: 'test' | 'prod' | 'work-server', baseline: ServerCommandItem | null) => {
for (let attempt = 0; attempt < 16; attempt += 1) {
await waitForDuration(2500);
@@ -1549,7 +1562,10 @@ export function MainHeader({
}
}
return { ok: false, item: key === 'test' ? testServerStatus : workServerStatus };
return {
ok: false,
item: key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus,
};
};
const handleResetClientState = async () => {
@@ -1589,14 +1605,19 @@ export function MainHeader({
}
};
const restartServerWithVerification = async (key: 'test' | 'work-server', busyKey: 'test' | 'work-server' | 'all') => {
const baseline = key === 'test' ? testServerStatus : workServerStatus;
const targetLabel = key === 'test' ? 'TEST 서버' : 'WORK 서버';
const restartServerWithVerification = async (
key: 'test' | 'prod' | 'work-server',
busyKey: 'test' | 'prod' | 'work-server' | 'all',
) => {
const baseline = key === 'test' ? testServerStatus : key === 'prod' ? prodServerStatus : workServerStatus;
const targetLabel = key === 'test' ? 'TEST 서버' : key === 'prod' ? 'PROD 컨테이너' : 'WORK 서버';
const result = await restartServerCommand(key);
if (key === 'test') {
setTestServerStatus(result.item);
} else if (key === 'prod') {
setProdServerStatus(result.item);
} else {
setWorkServerStatus(result.item);
}
@@ -1627,7 +1648,7 @@ export function MainHeader({
return true;
};
const handleRestartSingleServer = async (key: 'test' | 'work-server') => {
const handleRestartSingleServer = async (key: 'test' | 'prod' | 'work-server') => {
if (!hasAccess || serverRestartingKey) {
return false;
}
@@ -1639,7 +1660,7 @@ export function MainHeader({
try {
return await restartServerWithVerification(key, key);
} catch (error) {
const targetLabel = key === 'test' ? 'TEST 서버' : 'WORK 서버';
const targetLabel = key === 'test' ? 'TEST 서버' : key === 'prod' ? 'PROD 컨테이너' : 'WORK 서버';
setServerRestartFeedback({
tone: 'error',
message: error instanceof Error ? error.message : `${targetLabel} 재기동에 실패했습니다.`,
@@ -1650,6 +1671,67 @@ export function MainHeader({
}
};
const handleConfirmRestartProdServer = () => {
if (!hasAccess || serverRestartingKey) {
return;
}
modalApi.confirm({
title: 'PROD 빌드 반영',
content: 'PROD 컨테이너를 빌드 후 재기동합니다. 진행할까요?',
okText: '빌드 및 재기동',
cancelText: '취소',
okButtonProps: { danger: true },
onOk: async () => {
await handleRestartSingleServer('prod');
},
});
};
const handleRestartCommandRunner = async () => {
if (!hasAccess || serverRestartingKey) {
return;
}
setServerRestartCopyFeedback(null);
setServerRestartFeedback(null);
setServerRestartingKey('command-runner');
try {
const result = await restartServerCommand('command-runner');
setServerRestartFeedback({
tone: 'success',
message:
result.restartState === 'accepted'
? 'Command runner 배포 및 재기동 요청을 접수했습니다.'
: 'Command runner 배포 및 재기동을 완료했습니다.',
});
} catch (error) {
setServerRestartFeedback({
tone: 'error',
message: error instanceof Error ? error.message : 'Command runner 배포 및 재기동에 실패했습니다.',
});
} finally {
setServerRestartingKey(null);
}
};
const handleConfirmRestartCommandRunner = () => {
if (!hasAccess || serverRestartingKey) {
return;
}
modalApi.confirm({
title: 'Command runner 배포 및 재기동',
content: '현재 command runner를 다시 배포하고 재기동합니다. 진행할까요?',
okText: '배포 및 재기동',
cancelText: '취소',
onOk: async () => {
await handleRestartCommandRunner();
},
});
};
const handleRestartBothServers = async () => {
if (!hasAccess || serverRestartingKey) {
return;
@@ -1737,7 +1819,7 @@ export function MainHeader({
};
const handleResetNotificationIdentity = () => {
Modal.confirm({
modalApi.confirm({
title: '알림 클라이언트 초기화',
content: '현재 클라이언트의 알림 토큰/클라이언트 ID를 초기화합니다. 다시 접속하면 새로 등록됩니다.',
okText: '초기화',
@@ -2718,6 +2800,7 @@ export function MainHeader({
return (
<>
{modalContextHolder}
<Header className="app-header">
<Space size={12} className="app-header__row">
<Space size={12} className="app-header__menu-side">
@@ -3027,6 +3110,24 @@ export function MainHeader({
{activeAppSettingsSection === 'worklogAutomation' ? worklogAutomationPanel : null}
{activeAppSettingsSection === 'automationNotifications' ? automationNotificationPanel : null}
{activeAppSettingsSection === 'gestureShortcuts' ? gestureShortcutsPanel : null}
<Divider style={{ marginBlock: 4 }} />
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Text strong>Command runner</Text>
<Text type="secondary">
command runner .
</Text>
{renderFeedback(serverRestartFeedback, serverRestartCopyFeedback, setServerRestartCopyFeedback)}
<Button
block
type="primary"
icon={serverRestartingKey === 'command-runner' ? <ReloadOutlined spin /> : <ReloadOutlined />}
loading={serverRestartingKey === 'command-runner'}
disabled={!canRestartServers}
onClick={handleConfirmRestartCommandRunner}
>
command runner
</Button>
</Space>
</>
) : null}
{activeSettingsModal === 'notification' ? (
@@ -3154,6 +3255,17 @@ export function MainHeader({
<Text type="secondary">
: {getServerLastSourceChangedDateLabel(workServerStatus)}
</Text>
<Text type="secondary">
<span
className={`app-header__server-version-indicator ${getServerVersionStatusClassName(prodServerStatus)}`}
aria-label={getServerVersionStatusTitle(prodServerStatus, '운영')}
title={getServerVersionStatusTitle(prodServerStatus, '운영')}
style={{ marginInlineStart: 8, verticalAlign: 'middle' }}
aria-hidden="true"
/>
</Text>
<Text type="secondary">{formatDateTimeLabel(prodServerStatus?.runningBuiltAt ?? null)}</Text>
{renderFeedback(updateCheckFeedback, updateCheckCopyFeedback, setUpdateCheckCopyFeedback)}
<Button
block
@@ -3188,6 +3300,7 @@ export function MainHeader({
<Text strong style={{ marginTop: 8 }}>
</Text>
<Text type="secondary"> TEST와 WORK .</Text>
<Text type="secondary">
: {formatDateTimeLabel(testServerStatus?.checkedAt ?? null)}
</Text>
@@ -3234,6 +3347,24 @@ export function MainHeader({
</Button>
</Space>
<Text strong style={{ marginTop: 8 }}>
PROD
</Text>
<Text type="secondary"> : {formatDateTimeLabel(prodServerStatus?.checkedAt ?? null)}</Text>
<Text type="secondary">
PROD , .
</Text>
<Button
type="primary"
danger
block
icon={serverRestartingKey === 'prod' ? <ReloadOutlined spin /> : <ReloadOutlined />}
loading={serverRestartingKey === 'prod'}
disabled={!canRestartServers}
onClick={handleConfirmRestartProdServer}
>
PROD
</Button>
</Space>
) : null}
</Space>