import type { TFunction } from "i18next"; import type { JobResult } from "./api"; import { formatNumber } from "./format"; import { vocabKey } from "./vocab"; /** A notice from before the result rode along as data — and every audit row — holds only the server's * one-liner: `_summarize`'s "new=3, failed=0" (backend/app/scheduler.py). A flat one reads back into a * result, and `enrich`'s bare count ("50") into `{enriched: 50}` as `_result_data` names it; a nested * phase (a Python dict repr) or anything else does not, and stays as it was. */ export function parseSummary(summary: string | null | undefined): JobResult | null { if (!summary || /[{}[\]]/.test(summary)) return null; if (/^\d+$/.test(summary)) return { enriched: Number(summary) }; const out: JobResult = {}; for (const part of summary.split(", ")) { const m = /^(\w+)=(.+)$/.exec(part); if (!m) return null; const [, key, raw] = m; out[key!] = /^-?\d+$/.test(raw!) ? Number(raw) : raw!; } return out; } /** A scheduler job's result in the reader's language (E30): "új videó: 3 · sikertelen: 0". The server * stored "new=3, failed=0" — English keys, a Python dict repr for a nested phase. A count's key is * named from `scheduler.result`, a `skipped` / `reason` word from `scheduler.skip`; an unnamed key or * word shows as it is, and an `error` stays the upstream's own text (it is diagnostic). */ export function jobResultText(t: TFunction, result: JobResult, lang: string): string { return Object.entries(result) .map(([key, value]) => { const label = t(`scheduler.result.${key}`, { defaultValue: key.replace(/_/g, " ") }); if (typeof value === "number") return `${label}: ${formatNumber(value, lang)}`; if (typeof value === "string") return key === "error" ? `${label}: ${value}` : `${label}: ${t(`scheduler.skip.${vocabKey(value)}`, { defaultValue: value })}`; return `${label} (${jobResultText(t, value, lang)})`; }) .join(" · "); } /** 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). */ export function jobRunText( t: TFunction, run: { job_id: string; status?: unknown; summary?: unknown; result?: unknown }, lang: string, ): { title: string; body: string | null } { const job = t(`scheduler.jobs.${run.job_id}`, run.job_id); const summary = typeof run.summary === "string" && run.summary ? run.summary : null; const titleKey = run.status === "error" ? "inbox.jobDone.titleError" : run.status === "skipped" ? "inbox.jobDone.titleSkipped" : "inbox.jobDone.titleOk"; let result: JobResult | null = null; if (run.status !== "error") { result = run.result && typeof run.result === "object" ? (run.result as JobResult) : (parseSummary(summary) ?? (run.status === "skipped" && summary ? { skipped: summary } : null)); } // A skipped run's title already says "skipped": its body is only the reason, not "skipped: ". const reason = run.status === "skipped" ? onlySkipReason(result) : null; return { title: t(titleKey, { job }), body: reason !== null ? t(`scheduler.skip.${vocabKey(reason)}`, { defaultValue: reason }) : result ? jobResultText(t, result, lang) : summary, }; } function onlySkipReason(result: JobResult | null): string | null { const keys = result ? Object.keys(result) : []; const word = result?.skipped; return keys.length === 1 && typeof word === "string" ? word : null; }