diff --git a/scripts/assets/web-probe-sentinel-dashboard/dashboard.js b/scripts/assets/web-probe-sentinel-dashboard/dashboard.js index 60b3605e..2f956ab4 100644 --- a/scripts/assets/web-probe-sentinel-dashboard/dashboard.js +++ b/scripts/assets/web-probe-sentinel-dashboard/dashboard.js @@ -527,7 +527,7 @@ function renderFindingItemCollapsed(item) { const code = item.code || item.findingId || "finding"; const latestRunId = item.latestRunId || "-"; const hasLatestRun = latestRunId !== "-"; - const codeLabel = displayFindingCode(code); + const codeLabel = displayFindingTitle(item, code); const rootCause = findingRootCauseText(item); const evidence = findingEvidenceText(item); const nextAction = item.nextAction || findingNextAction(code); @@ -775,7 +775,7 @@ function detailFindings(findings) { ${escapeHtml(displaySeverity(item.severity))} ${escapeHtml(item.finding_id || item.findingId || "-")} ${escapeHtml(String(item.count ?? 0))} - ${escapeHtml(shortText(displayFindingSummary(item.finding_id || item.findingId || "", item.summary || ""), 220))} + ${escapeHtml(shortText(displayFindingTitle(item, item.finding_id || item.findingId || ""), 220))} ${escapeHtml(shortText(findingRootCauseText(item) || item.nextAction || "-", 240))} ${escapeHtml(shortText(item.report_json_sha256 || item.reportJsonSha256 || "-", 24))} `).join("")} @@ -1296,6 +1296,8 @@ function displayFindingCode(code) { const normalized = String(code || "").toLowerCase(); const labels = { "quick-verify-no-business-turn": "quick verify 未触达业务 turn", + "quick-verify-timeout-over-budget": "quick-verify 超时", + "observe-turn-terminal-wait-failed": "Workbench 等待终态超时", "workbench-turn-state-triad-inconsistent": "Workbench 状态三元组不一致", "session-rail-title-fallback-root-cause": "INV-02 会话标题 fallback 根因", "trace-events-page-read-404-root-cause": "INV-07 trace events 404 根因", @@ -1312,6 +1314,11 @@ function displayFindingCode(code) { return labels[normalized] || String(code || "finding"); } +function displayFindingTitle(item, code) { + const dynamicTitle = item?.displayTitleZh || item?.errorTitleZh || item?.timeoutDisplay?.titleZh; + return dynamicTitle ? String(dynamicTitle) : displayFindingCode(code); +} + function displayFindingSummary(code, summary) { const normalized = String(code || "").toLowerCase(); const summaries = { diff --git a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js index 4190d31c..11531850 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -1486,6 +1486,8 @@ function rootCauseText(item) { } function findingTitle(item) { + const dynamicTitle = safeUserText(item?.displayTitleZh || item?.errorTitleZh || item?.timeoutDisplay?.titleZh); + if (dynamicTitle) return dynamicTitle; const display = checkDisplay(item); if (display.title) return display.title; return safeUserText(item?.checkTitleZh || item?.check?.titleZh) || "未登记监测项"; @@ -1608,9 +1610,10 @@ function checkDisplay(item) { const rawCode = rawCheckCode(item); const rawId = rawFindingId(item); const serviceCode = publicCheckCode(item?.checkCode || item?.check?.code); + const dynamicTitle = safeUserText(item?.displayTitleZh || item?.errorTitleZh || item?.timeoutDisplay?.titleZh); return { code: serviceCode || publicCheckCode(rawId || rawCode) || stableCheckCode(rawCode || rawId), - title: safeUserText(item?.checkTitleZh || item?.check?.titleZh) || "未登记监测项", + title: dynamicTitle || safeUserText(item?.checkTitleZh || item?.check?.titleZh) || "未登记监测项", summary: safeUserText(item?.checkSummaryZh || item?.check?.summaryZh) || "已记录监测项详情,见报告原文。", action: safeUserText(item?.checkActionZh || item?.check?.actionZh) || "查看详情后处理", }; diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index d808ecde..8e9fe300 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -4382,10 +4382,11 @@ function renderControlPlaneResult(result: Record): string { "", Object.keys(sourceMirrorSync).length === 0 ? "SOURCE_MIRROR_SYNC\n-" : table(["OK", "PHASE", "JOB", "COMMIT", "STAGE_REF", "ELAPSED"], [[sourceMirrorSync.ok, sourceMirrorSync.phase, sourceMirrorSync.jobName, short(record(sourceMirrorSync.payload).mirrorCommit), short(record(sourceMirrorSync.payload).stageRef), sourceMirrorSync.elapsedMs ?? "-"]]), "", - Object.keys(targetValidation).length === 0 ? "TARGET_VALIDATION\n-" : table(["OK", "STATUS", "BUSINESS", "SCENARIO", "RUN", "OBSERVER", "REPORT", "FINDINGS", "ARTIFACTS"], [[ + Object.keys(targetValidation).length === 0 ? "TARGET_VALIDATION\n-" : table(["OK", "STATUS", "BUSINESS", "ERROR_TITLE", "SCENARIO", "RUN", "OBSERVER", "REPORT", "FINDINGS", "ARTIFACTS"], [[ targetValidation.ok, targetValidation.status, targetValidationBusiness.status ?? "-", + targetValidation.errorTitleZh ?? targetValidation.failureTitleZh ?? targetValidationBusiness.errorTitleZh ?? "-", targetValidation.scenarioId, targetValidation.runId, targetValidation.observerId, diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts index 3c6404ea..5bce8d02 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts @@ -191,6 +191,7 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string, const repeat = Math.max(1, typeof item.repeat === "number" && Number.isFinite(item.repeat) ? Math.trunc(item.repeat) : 1); for (let index = 0; index < repeat; index += 1) { if (Date.now() >= deadline) { + const timeoutDisplay = timeoutDisplayForQuickVerifyFailure("quick-verify-timeout-over-budget", promptIndex, warningBudgetSeconds, timeoutSeconds); printQuickVerifyProgress(state, runId, "timeout", "failed", { observerId, promptIndex, elapsedMs: elapsedMs(), hardBudgetSeconds }); return recordQuickVerify(state, finalizeQuickVerifyFailure(state, { runId, @@ -200,6 +201,8 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string, promptIndex, steps, failure: "quick-verify-timeout-over-budget", + failureTitleZh: stringAtNullable(timeoutDisplay, "titleZh"), + timeoutDisplay, elapsedMs: elapsedMs(), warnings: mergeWarnings(`quick verify exceeded the hard ${hardBudgetSeconds}s execution budget after the configured ${warningBudgetSeconds}s targetValidation warning budget.`, elapsedWarnings()), promptSource: prompts.summary, @@ -486,10 +489,15 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { readonly promptIndex: number; readonly steps: readonly Record[]; readonly failure: string; + readonly failureTitleZh?: string | null; + readonly timeoutDisplay?: Record | null; readonly promptSource?: Record; readonly elapsedMs?: number; readonly warnings?: readonly unknown[]; }): Record { + const targetValidationSeconds = numberAt(state.cicd, "targetValidation.maxSeconds"); + const failureDisplay = input.timeoutDisplay ?? timeoutDisplayForQuickVerifyFailure(input.failure, input.promptIndex, targetValidationSeconds, null); + const failureTitle = input.failureTitleZh ?? stringAtNullable(failureDisplay, "titleZh"); const cleanupSteps: Record[] = []; if (input.promptIndex > 0) { const cancel = runChildCli([ @@ -519,7 +527,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { const turnSummary = collectObserveView(state, input.observerId, "turn-summary", null, 30); const traceFrame = collectObserveView(state, input.observerId, "trace-frame", input.promptIndex > 0 ? input.promptIndex : null, 30); const durableBusinessTurn = quickVerifyHasDurableBusinessTurn(input.promptIndex, turnSummary, traceFrame); - const controlFindings = quickVerifyControlFindings(input.failure, input.promptIndex, turnSummary, traceFrame); + const controlFindings = quickVerifyControlFindings(input.failure, input.promptIndex, turnSummary, traceFrame, failureDisplay); const artifactSummaryRecord = record(artifactSummary); const artifactFindings = Array.isArray(artifactSummaryRecord.findings) ? artifactSummaryRecord.findings.map(record) : []; const visibilityFindings = quickVerifyAnalysisVisibilityFindings(analysis, artifactSummary); @@ -533,7 +541,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { && record(artifactSummary).ok === true && controlFindings.length === 0 && blockingFindings.length === 0; - const businessStatus = quickVerifyBusinessStatus(input.failure, input.promptIndex, turnSummary, traceFrame, input.elapsedMs ?? null, numberAt(state.cicd, "targetValidation.maxSeconds")); + const businessStatus = quickVerifyBusinessStatus(input.failure, input.promptIndex, turnSummary, traceFrame, input.elapsedMs ?? null, targetValidationSeconds); return { ok: recoveredWaitFailure, runId: input.runId, @@ -543,6 +551,9 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { observerId: input.observerId, elapsedMs: input.elapsedMs ?? null, businessStatus, + failureTitleZh: recoveredWaitFailure ? null : failureTitle, + errorTitleZh: recoveredWaitFailure ? null : failureTitle, + timeoutDisplay: recoveredWaitFailure ? null : failureDisplay, stateDir: indexEntry?.stateDir ?? null, reportJsonSha256: stringAtNullable(artifactSummary, "reportJsonSha256"), findingCount: findings.length, @@ -552,8 +563,8 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { steps: [...input.steps, ...cleanupSteps], analysis: artifactSummary, views: { - summary: { renderedText: renderQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, findings, findingCount: findings.length, steps: input.steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) }, - "auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, steps: input.steps, findings, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) }, + summary: { renderedText: renderQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, findings, findingCount: findings.length, steps: input.steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), failure: recoveredWaitFailure ? null : input.failure, failureTitleZh: recoveredWaitFailure ? null : failureTitle, timeoutDisplay: recoveredWaitFailure ? null : failureDisplay }) }, + "auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, steps: input.steps, findings, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), failure: recoveredWaitFailure ? null : input.failure, failureTitleZh: recoveredWaitFailure ? null : failureTitle, timeoutDisplay: recoveredWaitFailure ? null : failureDisplay }) }, "turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok }, "trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok }, }, @@ -563,7 +574,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { warnings: mergeWarnings( Array.isArray(input.warnings) ? input.warnings : [], recoveredWaitFailure ? ["quick verify wait command timed out, but collected turn-summary/trace-frame artifacts show a durable completed business turn; treating the wait timeout as a non-blocking tool finding."] : [], - targetValidationElapsedWarnings(input.elapsedMs ?? null, "quick verify confirm-wait", numberAt(state.cicd, "targetValidation.maxSeconds")), + targetValidationElapsedWarnings(input.elapsedMs ?? null, "quick verify confirm-wait", targetValidationSeconds), ), valuesRedacted: true, }; @@ -716,6 +727,9 @@ function recordQuickVerify(state: SentinelCicdState, payload: Record): boolean { }); } -function quickVerifyControlFindings(failure: string | null, promptIndex: number, turnSummary: Record | null, traceFrame: Record | null): Record[] { +function quickVerifyControlFindings(failure: string | null, promptIndex: number, turnSummary: Record | null, traceFrame: Record | null, display: Record | null = null): Record[] { if (quickVerifyHasDurableBusinessTurn(promptIndex, turnSummary, traceFrame)) return []; const turnDiagnostics = quickVerifyTurnSummaryDiagnostics(promptIndex, turnSummary); const traceDiagnostics = quickVerifyTraceFrameDiagnostics(traceFrame); @@ -1834,13 +1848,17 @@ function quickVerifyControlFindings(failure: string | null, promptIndex: number, if (noPromptScenario && failure === null) return []; if (noPromptScenario && failure !== null) { const observerStartFailure = failure === "observe-start-failed"; + const displayTitleZh = stringAtNullable(display ?? {}, "titleZh"); return [{ id: observerStartFailure ? "quick-verify-observer-start-failed" : "quick-verify-command-sequence-failed", severity: "red", count: 1, - summary: observerStartFailure + summary: displayTitleZh ?? (observerStartFailure ? "quick verify observer failed to start before the no-prompt scenario could run." - : "quick verify no-prompt command sequence failed before the account/session workflow completed.", + : "quick verify no-prompt command sequence failed before the account/session workflow completed."), + displayTitleZh, + errorTitleZh: displayTitleZh, + timeoutDisplay: display, failure, promptIndex, valuesRedacted: true, @@ -1849,11 +1867,15 @@ function quickVerifyControlFindings(failure: string | null, promptIndex: number, const noTrace = /无\s*sendPrompt|no\s+sendPrompt|无\s*trace\s*rows|no\s+trace\s+rows|traceId=-|routeSession=-|activeSession=-/iu.test(rendered); const emptyFinal = /Final Response[\s\S]*\(空内容\)/iu.test(rendered); if (!noTrace && !emptyFinal && failure !== "observe-start-failed") return []; + const displayTitleZh = stringAtNullable(display ?? {}, "titleZh"); return [{ id: "quick-verify-no-business-turn", severity: "red", count: 1, - summary: "quick verify did not reach a durable business turn/session/trace rows/final response; public dashboard health cannot be treated as HWLAB recovery.", + summary: displayTitleZh ?? "quick verify did not reach a durable business turn/session/trace rows/final response; public dashboard health cannot be treated as HWLAB recovery.", + displayTitleZh, + errorTitleZh: displayTitleZh, + timeoutDisplay: display, rootCause: `quick verify could not confirm a durable completed turn: turn-summary scopedRows=${String(turnDiagnostics.scopedRowCount ?? 0)} rowCount=${String(turnDiagnostics.rowCount ?? 0)}, traceFrame traceIdPresent=${traceDiagnostics.traceIdPresent === true} finalResponseEmpty=${traceDiagnostics.finalResponseEmpty === true}.`, rootCauseStatus: "confirmed", rootCauseConfidence: "high", @@ -2030,6 +2052,8 @@ function quickVerifyBusinessStatus( observerTimeout, scenarioComplete: durableBusinessTurn, failure: failure ?? null, + errorTitleZh: observerTimeout ? stringAtNullable(timeoutDisplayForQuickVerifyFailure(failure, promptIndex, budgetSeconds, null), "titleZh") : null, + timeoutDisplay: observerTimeout ? timeoutDisplayForQuickVerifyFailure(failure, promptIndex, budgetSeconds, null) : null, promptIndex, elapsedMs: elapsed, budgetSeconds, @@ -2044,6 +2068,42 @@ function isRecoverableQuickVerifyWaitFailure(failure: string): boolean { || failure === "observe-turn-terminal-wait-failed"; } +function timeoutDisplayForFailure(failure: string | null, phase: string, targetValidationSeconds: number | null, runnerTimeoutSeconds: number | null): Record { + const targetSeconds = typeof targetValidationSeconds === "number" && Number.isFinite(targetValidationSeconds) ? Math.max(1, Math.trunc(targetValidationSeconds)) : null; + const runnerSeconds = typeof runnerTimeoutSeconds === "number" && Number.isFinite(runnerTimeoutSeconds) ? Math.max(1, Math.trunc(runnerTimeoutSeconds)) : null; + const effectiveSeconds = targetSeconds ?? runnerSeconds; + const timeout = isTimeoutFailure(failure); + const phaseTitle = phase === "observe-wait-turn-terminal" ? "Workbench 等待终态" : phase === "observe-wait-startup-ready" ? "Workbench 启动等待" : "quick-verify"; + const titleZh = timeout && effectiveSeconds !== null ? `${phaseTitle} 超时(${effectiveSeconds} 秒)` : failure === null ? "" : `${phaseTitle} 失败`; + return { + titleZh, + budgetKind: targetSeconds !== null ? "targetValidation" : runnerSeconds !== null ? "runnerTimeoutSeconds" : "unknown", + budgetSeconds: effectiveSeconds, + targetValidationSeconds: targetSeconds, + runnerTimeoutSeconds: runnerSeconds, + phase, + failure, + valuesRedacted: true, + }; +} + +function timeoutDisplayForQuickVerifyFailure(failure: string | null, promptIndex: number, targetValidationSeconds: number | null, runnerTimeoutSeconds: number | null): Record { + if (!isTimeoutFailure(failure) || promptIndex <= 0) return timeoutDisplayForFailure(failure, "quick-verify", targetValidationSeconds, runnerTimeoutSeconds); + const base = timeoutDisplayForFailure(failure, "observe-wait-turn-terminal", targetValidationSeconds, runnerTimeoutSeconds); + const budgetSeconds = typeof base.budgetSeconds === "number" && Number.isFinite(base.budgetSeconds) ? Math.trunc(base.budgetSeconds) : null; + return { + ...base, + titleZh: budgetSeconds === null ? `Workbench 第 ${promptIndex} 轮等待终态超时` : `Workbench 第 ${promptIndex} 轮等待终态超时(${budgetSeconds} 秒)`, + round: promptIndex, + }; +} + +function isTimeoutFailure(failure: string | null): boolean { + return failure === "quick-verify-timeout-over-budget" + || failure === "quick-verify-wait-chunk-timeout" + || failure === "observe-turn-terminal-wait-failed"; +} + function compactCommandWithTail(result: CommandResult): CompactCommandResult & { stdoutTail: string; stderrTail: string } { return { ...compactCommand(result), @@ -2062,16 +2122,18 @@ function renderQuickVerifySummary(input: Record): string { ? artifact.findings.map(record).slice(0, 8) : []; const findingCount = numberAtNullable(input, "findingCount") ?? numberAtNullable(artifact, "findingCount") ?? findings.length; + const failureTitle = stringAtNullable(input, "failureTitleZh") ?? stringAtNullable(input, "errorTitleZh") ?? stringAtNullable(record(input.timeoutDisplay), "titleZh"); return [ "Web Probe Sentinel Quick Verify", "=======================================================", `run=${input.runId ?? "-"} scenario=${input.scenarioId ?? "-"} observer=${input.observerId ?? "-"}`, + failureTitle === null ? "" : `errorTitle=${failureTitle} failure=${input.failure ?? "-"}`, `report=${artifact.reportJsonSha256 ?? "-"} artifacts=${artifact.artifactCount ?? "-"} findings=${findingCount}`, `publicOrigin=${input.publicOrigin ?? "-"}`, "", "Findings", - findings.length === 0 ? "-" : findings.map((item) => `${item.severity ?? item.level ?? "-"} ${item.kind ?? item.id ?? item.code ?? "-"} count=${item.count ?? "-"}${formatQuickVerifyTimingSuffix(item)} ${item.summary ?? item.message ?? ""}`).join("\n"), - ].join("\n"); + findings.length === 0 ? "-" : findings.map(formatQuickVerifyFindingLine).join("\n"), + ].filter((line) => line !== "").join("\n"); } function renderAuthSessionSwitchQuickVerifySummary(input: Record): string { @@ -2099,6 +2161,7 @@ function renderAuthSessionSwitchQuickVerifySummary(input: Record `${item.severity ?? item.level ?? "-"} ${item.kind ?? item.id ?? item.code ?? "-"} count=${item.count ?? "-"}${formatQuickVerifyTimingSuffix(item)} ${item.summary ?? item.message ?? ""}`).join("\n"), - ].join("\n"); + findingRows.length === 0 ? "-" : findingRows.map(formatQuickVerifyFindingLine).join("\n"), + ].filter((line) => line !== "").join("\n"); +} + +function formatQuickVerifyFindingLine(item: Record): string { + const title = stringAtNullable(item, "displayTitleZh") ?? stringAtNullable(item, "errorTitleZh") ?? stringAtNullable(record(item.timeoutDisplay), "titleZh"); + const summary = title ?? item.summary ?? item.message ?? ""; + return `${item.severity ?? item.level ?? "-"} ${item.kind ?? item.id ?? item.code ?? "-"} count=${item.count ?? "-"}${formatQuickVerifyTimingSuffix(item)} ${summary}`; } function formatQuickVerifyTimingSuffix(item: Record): string { diff --git a/scripts/src/hwlab-node-web-sentinel-service.ts b/scripts/src/hwlab-node-web-sentinel-service.ts index 1ef9aead..872953ad 100644 --- a/scripts/src/hwlab-node-web-sentinel-service.ts +++ b/scripts/src/hwlab-node-web-sentinel-service.ts @@ -1004,6 +1004,9 @@ function dashboardFindings(config: WebProbeSentinelServiceConfig, db: Database, latestRunId: latestRun === null ? null : stringOrNull(latestRun.id), latestReportJsonSha256: latestRun === null ? null : stringOrNull(latestRun.report_json_sha256), summary: stringOrNull(row.summary), + displayTitleZh: stringOrNull(latestDetail?.displayTitleZh), + errorTitleZh: stringOrNull(latestDetail?.errorTitleZh), + timeoutDisplay: record(latestDetail?.timeoutDisplay), rootCause: stringOrNull(latestDetail?.rootCause), rootCauseStatus: stringOrNull(latestDetail?.rootCauseStatus), rootCauseConfidence: stringOrNull(latestDetail?.rootCauseConfidence), @@ -1860,6 +1863,9 @@ function enrichFindingRowWithStoredDetail(config: WebProbeSentinelServiceConfig, ...row, code: stringOrNull(row.finding_id), findingId: stringOrNull(row.finding_id), + displayTitleZh: stringOrNull(detail?.displayTitleZh), + errorTitleZh: stringOrNull(detail?.errorTitleZh), + timeoutDisplay: record(detail?.timeoutDisplay), rootCause: stringOrNull(detail?.rootCause), rootCauseStatus: stringOrNull(detail?.rootCauseStatus), rootCauseConfidence: stringOrNull(detail?.rootCauseConfidence), @@ -1904,6 +1910,9 @@ function compactStoredFinding(value: unknown): Record { severity, count: numberOr(item.count, numberOr(item.sampleCount, 1)), summary: summary.slice(0, 500), + displayTitleZh: stringOrNull(item.displayTitleZh), + errorTitleZh: stringOrNull(item.errorTitleZh), + timeoutDisplay: record(item.timeoutDisplay), rootCause: stringOrNull(item.rootCause), rootCauseStatus: stringOrNull(item.rootCauseStatus), rootCauseConfidence: stringOrNull(item.rootCauseConfidence), @@ -1982,6 +1991,9 @@ function enrichFindingWithCheck(config: WebProbeSentinelServiceConfig, value: Re checkTitleZh: stringOrNull(check.titleZh), checkSummaryZh: stringOrNull(check.summaryZh), checkActionZh: stringOrNull(check.actionZh), + displayTitleZh: stringOrNull(value.displayTitleZh) ?? stringOrNull(value.errorTitleZh), + errorTitleZh: stringOrNull(value.errorTitleZh) ?? stringOrNull(value.displayTitleZh), + timeoutDisplay: record(value.timeoutDisplay), checkRegistered: check.registered === true, blocking: value.blocking === true || check.blocking === true, valuesRedacted: true, @@ -2580,7 +2592,7 @@ function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view function formatStoredFindingLine(item: Record): string { const check = record(item.check); const code = stringOrNull(item.checkCode) ?? stringOrNull(check.code) ?? stringOrNull(item.finding_id) ?? stringOrNull(item.findingId) ?? stringOrNull(item.code) ?? "-"; - const title = stringOrNull(item.checkTitleZh) ?? stringOrNull(check.titleZh) ?? stringOrNull(item.summary) ?? ""; + const title = stringOrNull(item.displayTitleZh) ?? stringOrNull(item.errorTitleZh) ?? stringOrNull(record(item.timeoutDisplay).titleZh) ?? stringOrNull(item.checkTitleZh) ?? stringOrNull(check.titleZh) ?? stringOrNull(item.summary) ?? ""; const summary = stringOrNull(item.checkSummaryZh) ?? stringOrNull(check.summaryZh) ?? stringOrNull(item.summary) ?? ""; const rootCause = stringOrNull(item.rootCause); const status = stringOrNull(item.rootCauseStatus); @@ -2619,10 +2631,12 @@ function renderStoredSummary( const timing = dashboardRunTiming(config, row, stored); const reportSha = stringOrNull(row.report_json_sha256) ?? stringOrNull(artifactSummary.reportJsonSha256); const findingCount = visibleFindingTypeCount(row, findings, artifactSummary); + const errorTitle = stringOrNull(summary.errorTitleZh) ?? stringOrNull(summary.failureTitleZh) ?? stringOrNull(record(summary.timeoutDisplay).titleZh); return [ "Web Probe Sentinel Report", "=======================================================", `run=${stringOrNull(row.id) ?? "-"} scenario=${stringOrNull(row.scenario_id) ?? "-"} status=${stringOrNull(row.status) ?? "-"}`, + errorTitle === null ? "" : `errorTitle=${errorTitle} failure=${stringOrNull(summary.failure) ?? "-"}`, `observer=${stringOrNull(row.observer_id) ?? "-"} stateDir=${stringOrNull(row.state_dir) ?? "-"}`, `report=${reportSha ?? "-"} artifacts=${String(row.artifact_count ?? 0)} findings=${String(findingCount)}`, `timing=${formatRunTimingSummary(timing)}`, @@ -2631,7 +2645,7 @@ function renderStoredSummary( "", "Findings", findings.length === 0 ? "-" : findings.slice(0, 12).map(formatStoredFindingLine).join("\n"), - ].join("\n"); + ].filter((line) => line !== "").join("\n"); } function renderStoredFindings(row: Record, findings: readonly Record[], artifactSummary: Record): string {