fix(scheduler,i18n): a failure a job names reads in the reader's language

The audit log showed a failed RSS poll as the server's English sentence
under a Hungarian UI. A job can now raise progress.JobFailed(reason,
message, **params): the scheduler stores the reason and params as the
run's data (dashboard, manual-run notice, audit row) beside the English
line, and the client renders scheduler.failure.<reason>. The RSS outage
names its first channel and the reason code (HTTP 404 vs unreachable).
An upstream's own error text stays as raised (diagnostic).
This commit is contained in:
2026-10-01 22:51:37 +02:00
parent ec22eefd31
commit ab146211fa
10 changed files with 160 additions and 17 deletions
+26 -4
View File
@@ -38,10 +38,30 @@ export function jobResultText(t: TFunction, result: JobResult, lang: string): st
.join(" · ");
}
/** A failure the job named (the server's `progress.JobFailed`: `{failure: <reason>, …params}`) in the
* reader's language (E31); null for any other failure, which keeps the upstream's own text. */
export function jobFailureText(t: TFunction, result: unknown, lang: string): string | null {
if (!result || typeof result !== "object") return null;
const { failure, ...params } = result as Record<string, unknown>;
if (typeof failure !== "string") return null;
const values: Record<string, string> = {};
for (const [k, v] of Object.entries(params))
values[k] = typeof v === "number" ? formatNumber(v, lang) : String(v);
if (typeof params.cause === "string") values.cause = rssCause(t, params.cause);
return t(`scheduler.failure.${failure}`, { ...values, defaultValue: "" }) || null;
}
/** Why a feed did not answer, from the poll's reason code (`http_404`, `transport`, …). */
function rssCause(t: TFunction, cause: string): string {
const http = /^http_(\d+)$/.exec(cause);
if (http) return t("scheduler.rssCause.http", { status: http[1] });
return t(`scheduler.rssCause.${cause}`, { defaultValue: cause });
}
/** What a job run said, as the inbox notice and the audit row both show it (E30): a title naming the job
* and how the run ended, and its result in the reader's language — from the data when it rode along,
* else read back from the English one-liner. A skipped run names only why; a failed run keeps the
* upstream's own error text (it is diagnostic). */
* else read back from the English one-liner. A skipped run names only why; a failed run says the
* failure the job named (`jobFailureText`, E31), else keeps the upstream's own text (diagnostic). */
export function jobRunText(
t: TFunction,
run: { job_id: string; status?: unknown; summary?: unknown; result?: unknown },
@@ -65,10 +85,12 @@ export function jobRunText(
}
// A skipped run's title already says "skipped": its body is only the reason, not "skipped: <reason>".
const reason = run.status === "skipped" ? onlySkipReason(result) : null;
const failure = run.status === "error" ? jobFailureText(t, run.result, lang) : null;
return {
title: t(titleKey, { job }),
body:
reason !== null
body: failure
? failure
: reason !== null
? t(`scheduler.skip.${vocabKey(reason)}`, { defaultValue: reason })
: result
? jobResultText(t, result, lang)