Files
siftlode/frontend/src/lib/jobResult.ts
T
peter 9454717f47 fix(ui,i18n): E30 review round 2 — honest bio failure, a full show summary, one HU name per thing
- 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.
2026-10-01 08:31:04 +02:00

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;
}