feat(audit): job runs and config changes read in the reader's language (E30 S5)
The audit log showed the server's fixed English lines ("rss_poll: ok — new=5, failed=0",
"log_level = DEBUG", "Background sync paused") to a Hungarian reader. The details are rebuilt from the
row's data on the client: a job run names the job and its result as the inbox does, a config change
names the field and its values (on/off, grouped counts, "default"), a pause/resume adds nothing the
action does not say. A failed run keeps its error text (diagnostic). parseSummary reads enrich's bare
count ("50") back as `enriched`, as the server's `_result_data` names it — the audit and old notices
keep only the one-liner. No backend change; old rows render too.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import type { AuditRow } from "../lib/api";
|
||||
import { formatNumber } from "../lib/format";
|
||||
import { jobResultText, parseSummary } from "../lib/jobResult";
|
||||
import type { Column } from "./ui/DataTable";
|
||||
import { RelativeTime } from "./ui/RelativeTime";
|
||||
|
||||
@@ -12,6 +14,47 @@ function fmt(v: unknown): string {
|
||||
return v == null || v === "" ? "—" : String(v);
|
||||
}
|
||||
|
||||
/** A logged config value in the reader's language: a switch reads on/off, a count is grouped. */
|
||||
function configValue(t: TFunction, v: unknown, lang: string): string {
|
||||
if (typeof v === "boolean") return t(v ? "audit.on" : "audit.off");
|
||||
if (typeof v === "number") return formatNumber(v, lang);
|
||||
return fmt(v);
|
||||
}
|
||||
|
||||
/** The details of the rows whose server `summary` is a fixed English line (E30), rebuilt from the row's
|
||||
* data in the reader's language: a job run names the job and its result (as the inbox does), a config
|
||||
* change names the field and its values, a pause/resume says nothing the action has not. `undefined` =
|
||||
* not one of these; the generic summary + change rendering applies. */
|
||||
function localDetails(
|
||||
t: TFunction,
|
||||
row: AuditRow,
|
||||
lang: string,
|
||||
): { head: string | null; sub: string | null } | undefined {
|
||||
const after = row.after ?? {};
|
||||
const before = row.before ?? {};
|
||||
if (row.action === "job_run" && row.target_type === "job" && row.target_id) {
|
||||
const job = t(`scheduler.jobs.${row.target_id}`, row.target_id);
|
||||
const failed = after.status === "error";
|
||||
const summary = typeof after.summary === "string" ? after.summary : null;
|
||||
// A nested phase (a Python dict repr) or an error text does not parse, and shows as it was.
|
||||
const result = failed ? null : parseSummary(summary);
|
||||
return {
|
||||
head: t(failed ? "inbox.jobDone.titleError" : "inbox.jobDone.titleOk", { job }),
|
||||
sub: result ? jobResultText(t, result, lang) : summary,
|
||||
};
|
||||
}
|
||||
if ((row.action === "config_set" || row.action === "config_reset") && row.target_id) {
|
||||
const head = t(`config.fields.${row.target_id}.label`, row.target_id);
|
||||
// A secret's value is never logged: there is no before/after to show.
|
||||
if (!row.before && !row.after) return { head, sub: t("audit.secretUpdated") };
|
||||
const to =
|
||||
row.action === "config_reset" ? t("audit.default") : configValue(t, after.value, lang);
|
||||
return { head, sub: `${configValue(t, before.value, lang)} → ${to}` };
|
||||
}
|
||||
if (row.action === "sync_pause" || row.action === "sync_resume") return { head: null, sub: null };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** A compact before→after (or after-only) rendering of the JSON snapshots, e.g. "role: user → admin". */
|
||||
function changeText(row: AuditRow): string | null {
|
||||
const { before, after } = row;
|
||||
@@ -32,6 +75,7 @@ function changeText(row: AuditRow): string | null {
|
||||
* the data (built by the page) so the Action column's select filter only offers real values. */
|
||||
export function auditColumns(
|
||||
t: TFunction,
|
||||
lang: string,
|
||||
actionOptions: { value: string; label: string }[],
|
||||
): Column<AuditRow>[] {
|
||||
return [
|
||||
@@ -72,13 +116,15 @@ export function auditColumns(
|
||||
key: "details",
|
||||
header: t("audit.cols.details"),
|
||||
render: (r) => {
|
||||
const change = changeText(r);
|
||||
const local = localDetails(t, r, lang);
|
||||
const summary = local ? local.head : r.summary;
|
||||
const change = local ? local.sub : changeText(r);
|
||||
return (
|
||||
// wrap-break-word (wrap), NOT truncate: in the auto-layout table a `truncate` (nowrap) cell
|
||||
// sizes to its longest row, so one long summary/change stretched the whole table past the
|
||||
// card (the row rules bled into the page). Wrapping keeps every row inside the container.
|
||||
<div className="min-w-0">
|
||||
{r.summary && <div className="wrap-break-word">{r.summary}</div>}
|
||||
{summary && <div className="wrap-break-word">{summary}</div>}
|
||||
{change && <div className="text-2xs text-muted wrap-break-word">{change}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user