140 lines
5.2 KiB
HTML
140 lines
5.2 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>同步流水</title>
|
||
<link rel="stylesheet" href="/static/static.css">
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<header>
|
||
<h1>同步流水</h1>
|
||
<nav class="nav"><a href="/">首页</a><a href="/logs">文件日志</a><a href="/test">测试</a></nav>
|
||
</header>
|
||
<div class="toolbar">
|
||
<label>锚定日 <input type="date" id="anchor"></label>
|
||
<label>类型
|
||
<select id="step"></select>
|
||
</label>
|
||
<button class="primary" id="load">刷新</button>
|
||
<button type="button" class="danger" id="truncate" title="清空本机三表全部数据">清空三表</button>
|
||
</div>
|
||
<h3 style="color:var(--muted);font-size:0.95rem">同步任务</h3>
|
||
<div style="overflow:auto;margin-bottom:1.5rem">
|
||
<table><thead><tr>
|
||
<th>日期</th><th>类型</th><th>状态</th><th>总数</th><th>成功</th><th>失败</th><th>开始</th><th>结束</th>
|
||
</tr></thead><tbody id="jobs"></tbody></table>
|
||
</div>
|
||
<h3 style="color:var(--muted);font-size:0.95rem">业务记录(拉取 → 上报 → 回调)</h3>
|
||
<div style="overflow:auto">
|
||
<table><thead><tr>
|
||
<th>ID</th><th>日期</th><th>类型</th><th>批次</th><th>biz_key</th><th>拉取校验</th>
|
||
<th>上报</th><th>推送明文</th><th>HTTP</th><th>回调</th><th>trace</th><th></th>
|
||
</tr></thead><tbody id="items"></tbody></table>
|
||
</div>
|
||
</div>
|
||
<script src="/static/app.js"></script>
|
||
<script>
|
||
const anchorFromUrl = qs('anchor_date');
|
||
if (anchorFromUrl) document.getElementById('anchor').value = anchorFromUrl;
|
||
|
||
const truncateBtn = document.getElementById('truncate');
|
||
let pageMeta = null;
|
||
|
||
function updateTruncateButton(meta, syncRunning) {
|
||
if (!truncateBtn) return;
|
||
if (!meta || !meta.allowTruncate) {
|
||
truncateBtn.disabled = true;
|
||
truncateBtn.title = 'MySQL 未连接,无法清空';
|
||
return;
|
||
}
|
||
if (syncRunning) {
|
||
truncateBtn.disabled = true;
|
||
truncateBtn.title = '同步任务执行中,请稍后再试';
|
||
return;
|
||
}
|
||
truncateBtn.disabled = false;
|
||
truncateBtn.title = '清空本机 hy_push_log / hy_push_record / hy_sync_job(不可恢复)';
|
||
}
|
||
|
||
(async () => {
|
||
pageMeta = await loadMeta();
|
||
if (pageMeta && pageMeta.stepOptions) fillStepSelect(document.getElementById('step'), pageMeta.stepOptions);
|
||
updateTruncateButton(pageMeta, false);
|
||
})();
|
||
|
||
async function refreshSyncGate() {
|
||
try {
|
||
const s = await api('/api/test/status');
|
||
updateTruncateButton(pageMeta, !!(s && s.running));
|
||
} catch (_) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
setInterval(refreshSyncGate, 2000);
|
||
|
||
async function load() {
|
||
const anchor = document.getElementById('anchor').value;
|
||
const step = document.getElementById('step').value;
|
||
let url = '/api/runs?limit=100';
|
||
if (anchor) url += '&anchor_date=' + anchor;
|
||
if (step) url += '&step=' + step;
|
||
const data = await api(url);
|
||
|
||
document.getElementById('jobs').innerHTML = (data.jobs || []).map(j => `<tr>
|
||
<td>${esc(j.anchorDate)}</td>
|
||
<td>${esc(j.stepLabel || j.step)}</td>
|
||
<td>${badge(j.status, j.statusLabel)}</td>
|
||
<td>${j.totalCount}</td><td>${j.successCount}</td><td>${j.failedCount}</td>
|
||
<td>${fmtTimeAny(j.startedAt)}</td>
|
||
<td>${fmtTimeAny(j.finishedAt)}</td>
|
||
</tr>`).join('') || '<tr><td colspan="8">无任务</td></tr>';
|
||
|
||
document.getElementById('items').innerHTML = (data.items || []).map(it => {
|
||
const r = it.record;
|
||
const cb = it.callback;
|
||
const ve = it.errors && it.errors.valid ? (it.errors.formatted || it.errors.raw) : (it.errors && it.errors.raw) || '';
|
||
const veShort = ve.length > 80 ? ve.slice(0, 80) + '…' : ve;
|
||
const ll = r.LastLog;
|
||
return `<tr>
|
||
<td>${r.ID}</td>
|
||
<td>${esc(r.AnchorDate)}</td>
|
||
<td>${esc(it.stepLabel || it.step)}</td>
|
||
<td>${r.JobID}</td>
|
||
<td><code>${esc(r.BizKey)}</code></td>
|
||
<td>${veShort ? '<span class="badge warn">有</span> ' + esc(veShort) : badge('success', '通过')}</td>
|
||
<td>${badge(r.PushStatus, it.pushStatusLabel)}</td>
|
||
<td>${it.hasPushPlain ? '<span class="badge ok">有</span>' : '<span class="badge warn">无</span>'}</td>
|
||
<td>${ll ? ll.HTTPCode + ' / ' + (ll.MsgCode && ll.MsgCode.Valid ? ll.MsgCode.Int64 : '-') + ' / ' + ll.DurationMs + 'ms' : '-'}</td>
|
||
<td>${badge(cb.callback_status, it.callbackStatusLabel)} ${esc(cb.error_message || '')}</td>
|
||
<td>${ll ? esc(ll.TraceID) : '-'}</td>
|
||
<td><a href="/runs/record?id=${r.ID}">详情</a></td>
|
||
</tr>`;
|
||
}).join('') || '<tr><td colspan="12">无记录</td></tr>';
|
||
}
|
||
|
||
document.getElementById('load').onclick = () => load().catch(e => alert(e.message));
|
||
|
||
truncateBtn.onclick = async () => {
|
||
const tables = 'hy_push_log、hy_push_record、hy_sync_job';
|
||
if (!confirm('将清空本机审计库三表(' + tables + '),不可恢复。\n仅影响本机 xk_hy_transit,不动云端数据。\n\n确定继续?')) return;
|
||
const typed = prompt('请输入 TRUNCATE 以确认清空:');
|
||
if (typed !== 'TRUNCATE') {
|
||
if (typed !== null) alert('已取消:确认词不正确');
|
||
return;
|
||
}
|
||
try {
|
||
await api('/api/runs/truncate', { method: 'POST' });
|
||
alert('三表已清空');
|
||
await load();
|
||
} catch (e) {
|
||
alert(e.message);
|
||
}
|
||
};
|
||
|
||
load().catch(e => alert(e.message));
|
||
refreshSyncGate();
|
||
</script>
|
||
</body>
|
||
</html>
|