Files
siftlode/frontend/src/lib/jobResult.ts
T
peter 4aaaaa7860 chore(comments): drop epic, sprint and review-round ids from the code
The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").

Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.

Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).

One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
2026-10-05 00:19:39 +02:00

109 lines
5.3 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: "ú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(" · ");
}
/** A failure the job named (the server's `progress.JobFailed`: `{failure: <reason>, …params}`) in the
* reader's language; 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);
// A failure counted over `total` reads as a plural pair: i18next picks the form from a NUMERIC
// count, and the formatted values above are strings ("1 399" would pick none).
const count = typeof params.total === "number" ? params.total : undefined;
return t(`scheduler.failure.${failure}`, { ...values, count, 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: 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 says the
* failure the job named (`jobFailureText`), else keeps the upstream's own text (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;
const failure = run.status === "error" ? jobFailureText(t, run.result, lang) : null;
return {
title: t(titleKey, { job }),
body: failure
? failure
: 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;
}