fix: refresh public dashboard and BI telemetry

This commit is contained in:
Wyndham ARR
2026-08-03 16:08:14 +08:00
parent 2107f00e32
commit 5294edab89
20 changed files with 584 additions and 75 deletions

View File

@@ -20,6 +20,9 @@
companyReportConfirmRequest: null,
health: { database_ready: false, processing_ready: false, monthly_ready: false, download_ready: false, company_reports_ready: false, company_source_upload_ready: false },
analytics: null,
analyticsLoading: false,
biPollTimer: null,
biPollLoading: false,
toastTimer: null,
jobs: [],
selectedJobId: "",
@@ -55,6 +58,7 @@
const I18N = window.ARRI18n;
const TRACE_POLL_INTERVAL = 4000;
const MONTHLY_POLL_INTERVAL = 4000;
const BI_POLL_INTERVAL = 5000;
const HISTORY_PAGE_SIZE = 50;
const COMPANY_REVIEW_PAGE_SIZE = 50;
const COMPANY_REPORT_NAMES = ["LianTai", "QBD", "DY-AI-Easy-KB", "FengRun", "HanaTour"];
@@ -303,7 +307,12 @@
panel.classList.toggle("is-active", active);
});
if (updateHash) history.replaceState(null, "", `#${valid}`);
if (valid === "bi" && !state.analytics) loadAnalytics();
if (valid === "bi") {
if (!state.analytics) loadAnalytics();
scheduleBiPoll(250);
} else {
clearBiPoll();
}
if (valid === "usage" && state.csrf) {
loadCompanySource();
loadCompanyDraft();
@@ -672,6 +681,21 @@
state.monthlyPollTimer = null;
}
function clearBiPoll() {
window.clearTimeout(state.biPollTimer);
state.biPollTimer = null;
}
function biViewActive() {
return !document.hidden && $("#panel-bi")?.classList.contains("is-active");
}
function scheduleBiPoll(delay = BI_POLL_INTERVAL) {
clearBiPoll();
if (!biViewActive()) return;
state.biPollTimer = window.setTimeout(checkBiFreshness, delay);
}
function monthlyViewActive() {
return !document.hidden && $("#panel-monthly")?.classList.contains("is-active");
}
@@ -763,6 +787,30 @@
if (months.some((item) => item.month_key === current)) select.value = current;
}
async function checkBiFreshness() {
if (state.biPollLoading || !biViewActive()) {
scheduleBiPoll();
return;
}
state.biPollLoading = true;
try {
const months = await api("/api/months");
populateMonths(months);
const month = $("#bi-month").value || localMonth();
const metadata = months.find((item) => item.month_key === month);
const current = state.analytics;
const changed = !current
|| current.month_key !== month
|| Boolean(metadata && metadata.updated_at !== current.updated_at);
if (changed && !state.analyticsLoading) await loadAnalytics(false, Boolean(current));
} catch (_) {
// Keep the last good BI snapshot during a transient background failure.
} finally {
state.biPollLoading = false;
scheduleBiPoll();
}
}
function renderCompanySales(channels) {
const sorted = [...channels].sort((a, b) => Number(b.totals.total_price) - Number(a.totals.total_price));
const maximum = Math.max(1, ...sorted.map((item) => Number(item.totals.total_price || 0)));
@@ -832,13 +880,17 @@
$("#room-matrix").innerHTML = `<p class="empty-state">${escapeHtml(message)}</p>`;
}
async function loadAnalytics(showErrors = false) {
async function loadAnalytics(showErrors = false, preserveOnError = false) {
if (state.analyticsLoading) return;
state.analyticsLoading = true;
const month = $("#bi-month").value || localMonth();
try {
renderAnalytics(await api(`/api/analytics?month=${encodeURIComponent(month)}`));
} catch (error) {
resetAnalytics(I18N?.t("bi.empty") || "该月份暂无可用看板数据");
if (!preserveOnError) resetAnalytics(I18N?.t("bi.empty") || "该月份暂无可用看板数据");
if (showErrors) showToast(error.message, true);
} finally {
state.analyticsLoading = false;
}
}
@@ -1012,14 +1064,6 @@
|| `批次 #${batch} · ${rowCount} 条记录 · ${roomCount} 间 · 启用于 ${activated}`;
}
function companyJobSourceMeta(source) {
const batchId = Number(source?.source_batch_id);
const batch = Number.isInteger(batchId) && batchId > 0 ? formatInteger(batchId) : "—";
const activated = source?.activated_at ? formatDate(source.activated_at, true) : (I18N?.t("company.review_no_record") || "暂无");
return I18N?.t("company.job_source_meta", { batch, activated })
|| `批次 #${batch} · 启用于 ${activated}`;
}
function renderCompanySource(source) {
const panel = $("#company-source-current");
if (!panel) return;
@@ -1655,6 +1699,26 @@
return I18N?.t(key || "problem.review_result") || "需复核处理结果";
}
function companyProblemDetail(problem) {
if (problem?.code !== "COMPANY_REPORT_PUBLISH_FAILED") return "";
return I18N?.t("company.publish_failed_detail")
|| "数据已处理,但正式 Excel 保存失败。请重启报表服务后再生成。";
}
function companyPublishFailureSummary(job) {
const failedResults = (job?.company_results || []).filter((result) => result?.status === "failed");
const publishFailures = failedResults.filter((result) =>
(result.errors || []).some((problem) => problem?.code === "COMPANY_REPORT_PUBLISH_FAILED"),
);
if (!publishFailures.length) return "";
if (publishFailures.length === COMPANY_REPORT_NAMES.length) {
return I18N?.t("company.publish_failed_summary_all")
|| "数据已处理,但 5 家公司的正式 Excel 保存失败。请重启报表服务后再生成。";
}
return I18N?.t("company.publish_failed_summary_partial")
|| "数据已处理,但部分公司的正式 Excel 保存失败。请重启报表服务后再生成。";
}
function uniqueCompanyProblems(problems) {
const seen = new Set();
return (problems || []).filter((item) => {
@@ -1715,7 +1779,10 @@
}
const problems = companyProblemsForResult(result);
const problemHtml = problems.length
? problems.map((item) => `<span class="company-problem is-${item.kind}">${escapeHtml(companyProblemLabel(item.code))}<small>${escapeHtml(item.period || I18N?.t("company.review_no_record") || "暂无")}</small></span>`).join("")
? problems.map((item) => {
const detail = companyProblemDetail(item);
return `<span class="company-problem is-${item.kind}">${escapeHtml(companyProblemLabel(item.code))}<small>${escapeHtml(item.period || I18N?.t("company.review_no_record") || "暂无")}</small>${detail ? `<small class="company-problem-detail">${escapeHtml(detail)}</small>` : ""}</span>`;
}).join("")
: `<span class="cell-subtle">${escapeHtml(I18N?.t("company.review_no_record") || "暂无")}</span>`;
const download = result?.download_url?.startsWith("/api/company-reports/")
? `<a class="download-link" href="${escapeHtml(result.download_url)}">${escapeHtml(I18N?.t("common.download") || "下载")}</a>`
@@ -1746,7 +1813,7 @@
kicker.hidden = terminal;
$("#company-report-job-title").textContent = `${monthLabel(job.report_month)} · ${companyCutoffLabel(job)}`;
const message = $("#company-report-job-message");
const messageText = job.state === "failed" && !job.failure_code ? "" : (I18N?.errorMessage(job.failure_code, job.message) || I18N?.text(job.message || "") || job.message || "");
const messageText = (job.state === "failed" && !job.failure_code ? "" : (I18N?.errorMessage(job.failure_code, job.message) || I18N?.text(job.message || "") || job.message || "")) || companyPublishFailureSummary(job);
message.textContent = messageText;
message.hidden = !messageText;
const status = $("#company-report-job-state");
@@ -1762,16 +1829,6 @@
$("#company-report-job-as-of").textContent = job.as_of_date ? formatDate(job.as_of_date) : (I18N?.t("company.review_no_record") || "暂无");
$("#company-report-job-duration").textContent = formatDurationSeconds(job.duration_seconds);
$("#company-report-warning-count").textContent = I18N?.t("company.report_warning_count", { count: formatInteger(companyWarningCount(job)) }) || `${companyWarningCount(job)}`;
const source = job?.source;
const sourcePanel = $("#company-report-job-source");
sourcePanel.classList.toggle("is-missing", !source);
$("#company-report-job-source-file").textContent = source
? companySourceFilename(source)
: (I18N?.t("company.job_source_missing") || "历史任务未记录来源");
$("#company-report-job-source-file").title = source ? companySourceFilename(source) : "";
$("#company-report-job-source-meta").textContent = source
? companyJobSourceMeta(source)
: (I18N?.t("company.review_no_record") || "暂无");
$("#company-result-month-end-header").textContent = companyPeriodRange(job.report_month, "21-month-end");
renderCompanyResultRows(job);
localStorage.setItem("arr:last-company-report-job", job.job_id);
@@ -1783,13 +1840,10 @@
state.companyReportJobs = jobs;
const body = $("#company-history-rows");
if (!jobs.length) {
body.innerHTML = `<tr><td class="empty-cell" colspan="8">${historyEmptyMarkup("company", "history.company_empty", "暂无公司渠道明细任务")}</td></tr>`;
body.innerHTML = `<tr><td class="empty-cell" colspan="7">${historyEmptyMarkup("company", "history.company_empty", "暂无公司渠道明细任务")}</td></tr>`;
return;
}
body.innerHTML = jobs.map((job) => {
const source = job?.source;
const sourceFilename = source ? companySourceFilename(source) : (I18N?.t("company.job_source_missing") || "历史任务未记录来源");
const sourceMeta = source ? companyJobSourceMeta(source) : (I18N?.t("company.review_no_record") || "暂无");
return `<tr>
<td>${formatDate(job.created_at, true)}</td>
<td><strong>${escapeHtml(job.report_month ? monthLabel(job.report_month) : (I18N?.t("company.review_no_record") || "暂无"))}</strong></td>
@@ -1797,7 +1851,6 @@
<td><span class="status-chip ${companyStateStyle(job.state)}">${escapeHtml(companyStateLabel(job.state))}</span></td>
<td>${companySuccessCount(job)} / 5</td>
<td>${companyWarningCount(job)}</td>
<td><div class="company-history-source"><strong title="${escapeHtml(sourceFilename)}">${escapeHtml(sourceFilename)}</strong><small>${escapeHtml(sourceMeta)}</small></div></td>
<td><button class="company-detail-button" type="button" data-company-job-id="${escapeHtml(job.job_id)}">${escapeHtml(I18N?.t("common.view") || "查看")}</button></td>
</tr>`;
}).join("");
@@ -2221,9 +2274,11 @@
if (document.hidden) {
clearTracePoll();
clearMonthlyPoll();
clearBiPoll();
} else {
scheduleTracePoll(250);
if ($("#panel-monthly")?.classList.contains("is-active")) loadMonthly(false, true);
if ($("#panel-bi")?.classList.contains("is-active")) checkBiFreshness();
}
});
document.addEventListener("arr:locale-change", refreshLocalizedView);