- Person page: a bio ask that fails says so with a retry instead of reading as 'no biography'. - Show page: the summary is a ClampedText (5 lines + More), like the person and collection pages; one More/Less pair (plex.textMore/textLess) for all three. - Channels: a re-slotted row is placed in the language the server orders by (the stored preference, else English), not the UI's guess. - A skipped run's body is only the reason (its title already says skipped). - HU: Behúzási kvótatartalék; Eseménynapló ürítve / az eseménynaplót; indok; közvetlenül játszható / közvetlen közvetítés in the Plex hints. - Tests: ClampedText re-measures on a resize, the show summary clamp, serverOrderLang.
84 lines
3.8 KiB
TypeScript
84 lines
3.8 KiB
TypeScript
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: <reason>".
|
|
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;
|
|
}
|