From 584d082b6b959fcbc394e8281116ba7d4da2221a Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 29 Sep 2026 21:32:29 +0200 Subject: [PATCH] feat(inbox): a run notice from before the result rode along reads its one-liner back into one (E30 S2) --- .../src/components/NotificationsPanel.tsx | 5 ++-- frontend/src/lib/jobResult.dom.test.ts | 23 ++++++++++++++++++- frontend/src/lib/jobResult.ts | 15 ++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/NotificationsPanel.tsx b/frontend/src/components/NotificationsPanel.tsx index 4e261c6d..89532371 100644 --- a/frontend/src/components/NotificationsPanel.tsx +++ b/frontend/src/components/NotificationsPanel.tsx @@ -19,7 +19,7 @@ import { X, } from "lucide-react"; import { api, type AppNotification, type JobResult } from "../lib/api"; -import { jobResultText } from "../lib/jobResult"; +import { jobResultText, parseSummary } from "../lib/jobResult"; import { notifyActionError } from "../lib/actionErrors"; import { useLiveQuery } from "../lib/useLiveQuery"; import { focusAccessRequestsTab } from "../lib/adminUsersTab"; @@ -534,7 +534,8 @@ function NotificationRow({ title = t(d.status === "error" ? "inbox.jobDone.titleError" : "inbox.jobDone.titleOk", { job, }); - body = d.result ? jobResultText(t, d.result) : d.summary || n.body; + const result = d.result ?? parseSummary(d.summary); + body = result ? jobResultText(t, result) : d.summary || n.body; } else if (isEmailUndelivered) { // Email is fail-soft: a drop leaves no error anywhere else, so this notice IS the trace. Name // the message that was lost, since that is what tells the operator what their users didn't get. diff --git a/frontend/src/lib/jobResult.dom.test.ts b/frontend/src/lib/jobResult.dom.test.ts index 896ca189..a7d817fe 100644 --- a/frontend/src/lib/jobResult.dom.test.ts +++ b/frontend/src/lib/jobResult.dom.test.ts @@ -4,7 +4,7 @@ // one-liner was "recent={'channels_processed': 0, …}, deep={…}" — English keys and a Python repr. import { afterEach, describe, expect, it } from "vitest"; import i18n, { setLanguage } from "../i18n"; -import { jobResultText } from "./jobResult"; +import { jobResultText, parseSummary } from "./jobResult"; afterEach(() => setLanguage("en")); @@ -39,3 +39,24 @@ describe("a job's result", () => { ); }); }); + +describe("an older notice's one-liner", () => { + it("reads back into a result when flat", () => { + expect(parseSummary("new=403, failed=396, fallback=375")).toEqual({ + new: 403, + failed: 396, + fallback: 375, + }); + expect(parseSummary("enriched=0, skipped=tmdb_api_key not set")).toEqual({ + enriched: 0, + skipped: "tmdb_api_key not set", + }); + }); + + it("does not when a phase is nested, or it is no summary at all", () => { + expect(parseSummary("recent={'channels_processed': 0}, deep={'videos_added': 1}")).toBeNull(); + expect(parseSummary("sync paused")).toBeNull(); + expect(parseSummary("42")).toBeNull(); + expect(parseSummary(null)).toBeNull(); + }); +}); diff --git a/frontend/src/lib/jobResult.ts b/frontend/src/lib/jobResult.ts index 12cf1ca9..26e525bf 100644 --- a/frontend/src/lib/jobResult.ts +++ b/frontend/src/lib/jobResult.ts @@ -3,6 +3,21 @@ import type { JobResult } from "./api"; import { formatNumber } from "./format"; import { vocabKey } from "./vocab"; +/** A notice from before the result rode along as data 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; 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; + 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