From 237edf94d285ba25f611cc757483adcd2f707e19 Mon Sep 17 00:00:00 2001 From: Ivan Chirkin Date: Wed, 17 Jun 2026 13:50:40 +0300 Subject: [PATCH 1/5] feat(operations): cheap step-params line from last run --- cuprum-ui/src/lib/stepMetaLine.test.ts | 65 ++++++++++++++++++++++++++ cuprum-ui/src/lib/stepMetaLine.ts | 48 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 cuprum-ui/src/lib/stepMetaLine.test.ts create mode 100644 cuprum-ui/src/lib/stepMetaLine.ts diff --git a/cuprum-ui/src/lib/stepMetaLine.test.ts b/cuprum-ui/src/lib/stepMetaLine.test.ts new file mode 100644 index 00000000..09567787 --- /dev/null +++ b/cuprum-ui/src/lib/stepMetaLine.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "vitest"; +import { stepMetaLine, type StepMetaLabels } from "./stepMetaLine"; +import type { OperationRun } from "@/lib/api"; + +const L: StepMetaLabels = { + holes: "отв.", + tools: (n) => `${n} св.`, + dur: { h: "ч", m: "м", s: "с" }, + sec: "с", + side: (s) => (s === "top" ? "Верх" : "Низ"), +}; + +function run(p: Partial): OperationRun { + return { + runUid: "u", + projectPath: "/p", + opType: "drill", + startedAt: 0, + endedAt: 100, + outcome: "completed", + progressTotal: 84, + progressDone: 84, + paramsJson: "{}", + summaryJson: null, + ...p, + }; +} + +describe("stepMetaLine", () => { + it("null run → null", () => { + expect(stepMetaLine(null, L)).toBeNull(); + }); + it("drill: holes + tools + estimate", () => { + const r = run({ opType: "drill", progressTotal: 84, paramsJson: JSON.stringify({ toolCount: 2, estimateSec: 74 }) }); + expect(stepMetaLine(r, L)).toBe("84 отв. · 2 св. · ≈ 1 м 14 с"); + }); + it("drill: holes only when params empty", () => { + const r = run({ opType: "drill", progressTotal: 84, paramsJson: "{}" }); + expect(stepMetaLine(r, L)).toBe("84 отв."); + }); + it("drill: skips zero/absent estimate", () => { + const r = run({ opType: "drill", progressTotal: 10, paramsJson: JSON.stringify({ toolCount: 1, estimateSec: 0 }) }); + expect(stepMetaLine(r, L)).toBe("10 отв. · 1 св."); + }); + it("drill: malformed params → holes only, no crash", () => { + const r = run({ opType: "drill", progressTotal: 12, paramsJson: "{not json" }); + expect(stepMetaLine(r, L)).toBe("12 отв."); + }); + it("expose: side + exposure seconds", () => { + const r = run({ opType: "expose", progressTotal: null, paramsJson: JSON.stringify({ side: "top", exposureS: 45 }) }); + expect(stepMetaLine(r, L)).toBe("Верх · 45 с"); + }); + it("expose: bottom only when no exposure", () => { + const r = run({ opType: "expose", progressTotal: null, paramsJson: JSON.stringify({ side: "bottom" }) }); + expect(stepMetaLine(r, L)).toBe("Низ"); + }); + it("mill → null", () => { + const r = run({ opType: "mill", progressTotal: null, paramsJson: "{}" }); + expect(stepMetaLine(r, L)).toBeNull(); + }); + it("drill with nothing extractable → null", () => { + const r = run({ opType: "drill", progressTotal: null, paramsJson: "{}" }); + expect(stepMetaLine(r, L)).toBeNull(); + }); +}); diff --git a/cuprum-ui/src/lib/stepMetaLine.ts b/cuprum-ui/src/lib/stepMetaLine.ts new file mode 100644 index 00000000..54546a54 --- /dev/null +++ b/cuprum-ui/src/lib/stepMetaLine.ts @@ -0,0 +1,48 @@ +import type { OperationRun } from "@/lib/api"; +import { formatDuration, type DurationLabels } from "@/lib/runHistoryFormat"; + +/** Labels resolved by the caller (component owns the i18n namespace; keeping i18n + * out of this module lets the static i18n checker scope keys correctly). */ +export interface StepMetaLabels { + /** "отв." */ + holes: string; + /** Pluralized tool count, e.g. (2) => "2 сверла". */ + tools: (n: number) => string; + /** {h,m,s} short unit labels for the estimate. */ + dur: DurationLabels; + /** "с" (seconds suffix for exposure time). */ + sec: string; + /** Copper side label, e.g. ("top") => "Верх". */ + side: (s: string) => string; +} + +/** Cheap per-step parameter line from the LAST run (no fresh planner). Returns null + * when there's no run or nothing extractable. drill → holes · tools · ≈estimate; + * expose → side · exposure; mill (not journalled) and everything else → null. */ +export function stepMetaLine(run: OperationRun | null, L: StepMetaLabels): string | null { + if (!run) return null; + const parts: string[] = []; + + if (run.opType === "drill") { + if (run.progressTotal != null) parts.push(`${run.progressTotal} ${L.holes}`); + try { + const p = JSON.parse(run.paramsJson) as { toolCount?: number; estimateSec?: number }; + if (p.toolCount != null && p.toolCount > 0) parts.push(L.tools(p.toolCount)); + if (p.estimateSec != null && p.estimateSec > 0) { + parts.push(`≈ ${formatDuration(p.estimateSec, L.dur)}`); + } + } catch { + /* ignore malformed params */ + } + } else if (run.opType === "expose") { + try { + const p = JSON.parse(run.paramsJson) as { side?: string; exposureS?: number }; + if (p.side) parts.push(L.side(p.side)); + if (p.exposureS != null && p.exposureS > 0) parts.push(`${p.exposureS} ${L.sec}`); + } catch { + /* ignore malformed params */ + } + } + + return parts.length ? parts.join(" · ") : null; +} From 52e47b2e5c84e3e96675ef1be834e40d67f9314e Mon Sep 17 00:00:00 2001 From: Ivan Chirkin Date: Wed, 17 Jun 2026 13:51:13 +0300 Subject: [PATCH 2/5] feat(operations): i18n keys for step-params line --- cuprum-ui/src/locales/en/project.json | 7 +++++++ cuprum-ui/src/locales/ru/project.json | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/cuprum-ui/src/locales/en/project.json b/cuprum-ui/src/locales/en/project.json index d190fbfd..08e99ad5 100644 --- a/cuprum-ui/src/locales/en/project.json +++ b/cuprum-ui/src/locales/en/project.json @@ -293,6 +293,13 @@ "hole": "Hole", "tool": "Drill Ø{{mm}} mm", "stop": "Stop" + }, + "meta": { + "holes": "holes", + "tools_one": "{{count}} drill", + "tools_few": "{{count}} drills", + "tools_many": "{{count}} drills", + "side": { "top": "Top", "bottom": "Bottom" } } }, "setup": { diff --git a/cuprum-ui/src/locales/ru/project.json b/cuprum-ui/src/locales/ru/project.json index 58991165..0432a646 100644 --- a/cuprum-ui/src/locales/ru/project.json +++ b/cuprum-ui/src/locales/ru/project.json @@ -293,6 +293,13 @@ "hole": "Отверстие", "tool": "Сверло Ø{{mm}} мм", "stop": "Стоп" + }, + "meta": { + "holes": "отв.", + "tools_one": "{{count}} сверло", + "tools_few": "{{count}} сверла", + "tools_many": "{{count}} свёрл", + "side": { "top": "Верх", "bottom": "Низ" } } }, "setup": { From 292a3c48bba66d35e1f5a844d2d25996ef2aa488 Mon Sep 17 00:00:00 2001 From: Ivan Chirkin Date: Wed, 17 Jun 2026 13:52:16 +0300 Subject: [PATCH 3/5] feat(operations): render step-params line in StepCard --- .../src/components/operations/StepCard.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cuprum-ui/src/components/operations/StepCard.tsx b/cuprum-ui/src/components/operations/StepCard.tsx index 2bd63fd1..1d3f0761 100644 --- a/cuprum-ui/src/components/operations/StepCard.tsx +++ b/cuprum-ui/src/components/operations/StepCard.tsx @@ -1,9 +1,10 @@ import { useTranslation } from "react-i18next"; -import { Play, Eye, CheckCircle2 } from "lucide-react"; +import { Play, Eye, CheckCircle2, Wrench } from "lucide-react"; import type { OperationKind } from "@/lib/operationKind"; import type { OperationRun } from "@/lib/api"; import { relativeTime } from "@/i18n/relativeTime"; import { formatDuration } from "@/lib/runHistoryFormat"; +import { stepMetaLine } from "@/lib/stepMetaLine"; export function StepCard({ op, @@ -20,6 +21,14 @@ export function StepCard({ const Icon = op.icon; const ready = op.mode === "ready"; + const meta = stepMetaLine(lastRun, { + holes: t("operations.meta.holes"), + tools: (n) => t("operations.meta.tools", { count: n }), + dur: { h: t("runHistory.hourShort"), m: t("runHistory.minShort"), s: t("runHistory.secShort") }, + sec: t("runHistory.secShort"), + side: (s) => t(s === "top" ? "operations.meta.side.top" : "operations.meta.side.bottom"), + }); + return (
+ {meta && ( +
+ + {meta} +
+ )} +