fix: surface install progress behind reverse proxies
Signed Release / release (push) Failing after 32s
Signed Release / release (push) Failing after 32s
This commit is contained in:
@@ -190,6 +190,44 @@
|
||||
.progress-item.running .progress-dot { border-color: #2f6fed; background: #2f6fed; box-shadow: 0 0 0 4px #eaf1fd; }
|
||||
.progress-item.done { color: #187149; }
|
||||
.progress-item.done .progress-dot { border-color: #25805a; background: #25805a; }
|
||||
.install-progress { margin-top: 22px; padding-top: 18px; border-top: 1px solid #e8ebf0; }
|
||||
.install-progress-head, .install-progress-meta, .install-console-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.install-progress-label { color: #334057; font-size: 13px; font-weight: 650; }
|
||||
.install-progress-value { color: #1d3557; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.install-progress-track {
|
||||
height: 10px;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #e5eaf1;
|
||||
}
|
||||
.install-progress-bar {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background: #2f6fed;
|
||||
transition: width .35s ease;
|
||||
}
|
||||
.install-progress-meta { margin-top: 8px; color: #707c90; font-size: 12px; }
|
||||
.install-console { margin-top: 14px; overflow: hidden; border: 1px solid #28364a; border-radius: 6px; background: #182233; }
|
||||
.install-console-head { min-height: 36px; padding: 0 12px; border-bottom: 1px solid #314158; background: #111a28; }
|
||||
.install-console-title { color: #dce5f2; font-size: 12px; font-weight: 650; }
|
||||
.install-console-state { color: #7dd3a8; font-size: 12px; }
|
||||
.install-log {
|
||||
height: 148px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
color: #c9d5e5;
|
||||
font: 12px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
letter-spacing: 0;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.complete-mark {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
@@ -220,6 +258,8 @@
|
||||
.summary-row { grid-template-columns: 1fr; gap: 5px; }
|
||||
.actions { flex-wrap: wrap-reverse; }
|
||||
.btn { flex: 1 1 120px; }
|
||||
.install-progress-meta { align-items: flex-start; flex-direction: column; gap: 4px; }
|
||||
.install-log { height: 172px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -299,6 +339,23 @@
|
||||
<div class="progress-item" data-progress="3"><span class="progress-dot"></span><span>创建管理员账号</span></div>
|
||||
<div class="progress-item" data-progress="4"><span class="progress-dot"></span><span>启动正式服务并写入安装锁</span></div>
|
||||
</div>
|
||||
<div class="install-progress">
|
||||
<div class="install-progress-head">
|
||||
<span class="install-progress-label" id="install-phase">准备安装</span>
|
||||
<strong class="install-progress-value" id="install-percent">0%</strong>
|
||||
</div>
|
||||
<div class="install-progress-track" id="install-progress-track" role="progressbar" aria-label="安装进度" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
||||
<div class="install-progress-bar" id="install-progress-bar"></div>
|
||||
</div>
|
||||
<div class="install-progress-meta">
|
||||
<span id="install-elapsed">本页已等待 0 秒</span>
|
||||
<span id="install-check">等待安装任务</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-console">
|
||||
<div class="install-console-head"><span class="install-console-title">安装状态</span><span class="install-console-state" id="install-console-state">运行中</span></div>
|
||||
<pre class="install-log" id="install-log" aria-live="polite"></pre>
|
||||
</div>
|
||||
<div id="install-message" class="notice info">正在初始化,数据库规模较大时可能需要几分钟。</div>
|
||||
</div>
|
||||
<div id="complete-view" class="hidden">
|
||||
@@ -323,7 +380,18 @@
|
||||
history.replaceState(null, '', clean);
|
||||
}
|
||||
|
||||
const state = { step: 1, database: null, databaseFingerprint: '', administrator: null, installing: false };
|
||||
const state = {
|
||||
step: 1,
|
||||
database: null,
|
||||
databaseFingerprint: '',
|
||||
administrator: null,
|
||||
installing: false,
|
||||
installStartedAt: 0,
|
||||
installPercent: 0,
|
||||
installLogEntries: [],
|
||||
healthPolling: false,
|
||||
};
|
||||
const FORMAL_SERVICE_TIMEOUT_MS = 240000;
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
|
||||
|
||||
@@ -421,24 +489,95 @@
|
||||
});
|
||||
}
|
||||
|
||||
function formatElapsed(milliseconds) {
|
||||
const seconds = Math.max(0, Math.floor(milliseconds / 1000));
|
||||
if (seconds < 60) return `${seconds} 秒`;
|
||||
return `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒`;
|
||||
}
|
||||
|
||||
function setInstallMeter(percent, phase, detail) {
|
||||
const value = Math.max(state.installPercent, Math.max(0, Math.min(100, Math.round(percent))));
|
||||
state.installPercent = value;
|
||||
$('#install-progress-bar').style.width = `${value}%`;
|
||||
$('#install-percent').textContent = `${value}%`;
|
||||
$('#install-phase').textContent = phase;
|
||||
$('#install-check').textContent = detail;
|
||||
$('#install-elapsed').textContent = `本页已等待 ${formatElapsed(Date.now() - state.installStartedAt)}`;
|
||||
const track = $('#install-progress-track');
|
||||
track.setAttribute('aria-valuenow', String(value));
|
||||
track.setAttribute('aria-valuetext', `${phase},${value}%`);
|
||||
}
|
||||
|
||||
function appendInstallLog(message) {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
state.installLogEntries.push(`[${timestamp}] ${message}`);
|
||||
state.installLogEntries = state.installLogEntries.slice(-16);
|
||||
const output = $('#install-log');
|
||||
output.textContent = state.installLogEntries.join('\n');
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function beginInstallTracking(resumed) {
|
||||
state.installStartedAt = Date.now();
|
||||
state.installLogEntries = [];
|
||||
$('#install-message').className = 'notice info';
|
||||
$('#install-console-state').textContent = '运行中';
|
||||
if (resumed) {
|
||||
setInstallMeter(70, '恢复安装任务', '准备检查正式服务');
|
||||
appendInstallLog('检测到尚未完成的安装任务,继续等待正式服务。');
|
||||
} else {
|
||||
setInstallMeter(5, '提交安装任务', '准备验证数据库');
|
||||
appendInstallLog('安装请求已提交,正在验证 PostgreSQL 连接和权限。');
|
||||
}
|
||||
}
|
||||
|
||||
async function pollFormalService() {
|
||||
if (state.healthPolling) return;
|
||||
state.healthPolling = true;
|
||||
if (!state.installStartedAt) beginInstallTracking(true);
|
||||
setProgress(4);
|
||||
$('#install-message').textContent = '初始化完成,正在等待正式服务通过健康检查。';
|
||||
const deadline = Date.now() + 240000;
|
||||
setInstallMeter(Math.max(state.installPercent, 72), '启动正式服务', '等待第 1 次健康检查');
|
||||
appendInstallLog('数据库初始化完成,安装器正在切换到正式服务。');
|
||||
$('#install-message').textContent = '初始化完成,正在等待正式服务通过健康检查。页面会持续显示检查进度。';
|
||||
const healthStartedAt = Date.now();
|
||||
const deadline = healthStartedAt + FORMAL_SERVICE_TIMEOUT_MS;
|
||||
let attempt = 0;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
attempt += 1;
|
||||
const healthElapsed = Date.now() - healthStartedAt;
|
||||
const estimated = 72 + Math.floor((healthElapsed / FORMAL_SERVICE_TIMEOUT_MS) * 23);
|
||||
setInstallMeter(Math.min(95, estimated), '等待正式服务健康检查', `第 ${attempt} 次检查`);
|
||||
try {
|
||||
const response = await fetch('/api/oa/health', { cache: 'no-store' });
|
||||
const body = await response.json();
|
||||
if (response.ok && (body?.data?.status === 'UP' || body?.status === 'UP')) {
|
||||
const serviceStatus = body?.data?.status || body?.status || `HTTP ${response.status}`;
|
||||
const installLocked = body?.data?.installLocked ?? body?.installLocked;
|
||||
if (response.ok && serviceStatus === 'UP' && (installLocked === true || installLocked === 'true')) {
|
||||
setInstallMeter(100, '安装完成', '正式服务已就绪');
|
||||
appendInstallLog(`健康检查第 ${attempt} 次通过,安装锁已确认。`);
|
||||
$('#install-console-state').textContent = '已完成';
|
||||
$$('[data-progress]').forEach((item) => { item.classList.remove('running'); item.classList.add('done'); });
|
||||
$('#installing-view').classList.add('hidden');
|
||||
$('#complete-view').classList.remove('hidden');
|
||||
sessionStorage.removeItem('kaidi.setup.token');
|
||||
state.healthPolling = false;
|
||||
return;
|
||||
}
|
||||
} catch { /* Service is changing from installer to the formal application. */ }
|
||||
if (response.ok && serviceStatus === 'UP') {
|
||||
setInstallMeter(98, '正式服务已通过健康检查', '等待写入安装锁');
|
||||
appendInstallLog(`健康检查第 ${attempt} 次通过,正在等待后台写入安装锁。`);
|
||||
continue;
|
||||
}
|
||||
appendInstallLog(`健康检查第 ${attempt} 次:服务状态 ${serviceStatus}。`);
|
||||
} catch {
|
||||
appendInstallLog(`健康检查第 ${attempt} 次:服务端口正在切换,继续等待。`);
|
||||
}
|
||||
}
|
||||
state.healthPolling = false;
|
||||
setInstallMeter(95, '正式服务尚未就绪', `${Math.round(FORMAL_SERVICE_TIMEOUT_MS / 1000)} 秒检查超时`);
|
||||
appendInstallLog('健康检查超时,安装状态已保留,未写入完成锁。');
|
||||
$('#install-console-state').textContent = '需要检查';
|
||||
$('#install-message').className = 'notice error';
|
||||
$('#install-message').textContent = '正式服务尚未就绪。请查看 systemctl 日志或 /var/lib/kaidi-erp/install-formal.log;安装文件和待确认状态均已保留。';
|
||||
}
|
||||
@@ -506,10 +645,16 @@
|
||||
state.installing = true;
|
||||
$('#confirm-view').classList.add('hidden');
|
||||
$('#installing-view').classList.remove('hidden');
|
||||
beginInstallTracking(false);
|
||||
setProgress(1);
|
||||
let migrationTimer;
|
||||
try {
|
||||
const request = { database: state.database, administrator: state.administrator };
|
||||
setTimeout(() => setProgress(2), 700);
|
||||
migrationTimer = setTimeout(() => {
|
||||
setProgress(2);
|
||||
setInstallMeter(28, '执行数据库初始化', '迁移结构和基础数据');
|
||||
appendInstallLog('数据库连接验证通过,正在执行结构迁移和基础数据初始化。');
|
||||
}, 700);
|
||||
try {
|
||||
await api('/api/install/complete', { method: 'POST', body: JSON.stringify(request) });
|
||||
} catch (error) {
|
||||
@@ -518,12 +663,20 @@
|
||||
// is flushed; the formal health probe is the authoritative result.
|
||||
if (!(error instanceof TypeError) && error.message !== '安装服务响应无效') throw error;
|
||||
}
|
||||
clearTimeout(migrationTimer);
|
||||
setProgress(3);
|
||||
setInstallMeter(68, '创建管理员并切换服务', '数据库初始化已完成');
|
||||
appendInstallLog('数据库迁移和管理员初始化已提交,准备启动正式服务。');
|
||||
state.administrator.password = '';
|
||||
state.database.password = '';
|
||||
await pollFormalService();
|
||||
} catch (error) {
|
||||
clearTimeout(migrationTimer);
|
||||
state.installing = false;
|
||||
state.healthPolling = false;
|
||||
setInstallMeter(state.installPercent, '安装已中断', '请处理错误后重试');
|
||||
appendInstallLog(`安装中断:${error.message}`);
|
||||
$('#install-console-state').textContent = '已中断';
|
||||
$('#install-message').className = 'notice error';
|
||||
$('#install-message').textContent = error.message;
|
||||
const actions = document.createElement('div');
|
||||
@@ -548,6 +701,7 @@
|
||||
go(4);
|
||||
$('#confirm-view').classList.add('hidden');
|
||||
$('#installing-view').classList.remove('hidden');
|
||||
beginInstallTracking(true);
|
||||
return pollFormalService();
|
||||
}
|
||||
$('#java-detail').textContent = `Java ${status.javaVersion},最低要求 Java ${status.minimumJava}`;
|
||||
|
||||
Reference in New Issue
Block a user