Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion cuprum-ui/src/components/operations/StepCard.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Не подставляйте bottom для неизвестного side.

Сейчас любое значение, отличное от "top", отображается как "bottom", что даёт неверную мета-строку при неожиданных данных.

Предлагаемый фикс
-    side: (s) => t(s === "top" ? "operations.meta.side.top" : "operations.meta.side.bottom"),
+    side: (s) => {
+      if (s === "top") return t("operations.meta.side.top");
+      if (s === "bottom") return t("operations.meta.side.bottom");
+      return s;
+    },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuprum-ui/src/components/operations/StepCard.tsx` at line 29, The side
property function in StepCard.tsx currently uses a simple ternary operator that
defaults any value other than "top" to "bottom", which causes incorrect
translations for unexpected side values. Replace this logic to explicitly check
for both "top" and "bottom" values separately, and add proper handling for
unexpected or invalid side values (such as returning undefined or a fallback
translation). This ensures the component only applies the bottom translation
when the side value is explicitly "bottom", not for any unknown or invalid
input.

});

return (
<div
role="button"
Expand Down Expand Up @@ -56,6 +65,13 @@ export function StepCard({
</div>
</div>

{meta && (
<div className="mt-3.5 flex items-center gap-2 border-t border-border/60 pt-3 text-[11.5px] tabular-nums text-muted-foreground">
<Wrench className="size-3.5 shrink-0" />
<span className="truncate">{meta}</span>
</div>
)}

<div className="mt-3.5 flex items-center justify-between gap-3 border-t border-border/60 pt-3">
<LastRun run={lastRun} />
<button
Expand Down
65 changes: 65 additions & 0 deletions cuprum-ui/src/lib/stepMetaLine.test.ts
Original file line number Diff line number Diff line change
@@ -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>): 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();
});
});
48 changes: 48 additions & 0 deletions cuprum-ui/src/lib/stepMetaLine.ts
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 8 additions & 0 deletions cuprum-ui/src/locales/en/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,14 @@
"hole": "Hole",
"tool": "Drill Ø{{mm}} mm",
"stop": "Stop"
},
"meta": {
"holes": "holes",
"tools_one": "{{count}} drill",
"tools_few": "{{count}} drills",
"tools_many": "{{count}} drills",
"tools_other": "{{count}} drills",
"side": { "top": "Top", "bottom": "Bottom" }
}
Comment on lines +297 to 304

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== i18n init / plural options =="
rg -n -C3 'i18next|init\(|plural|compatibilityJSON|pluralSeparator' cuprum-ui/src

echo
echo "== tools plural keys in locales =="
rg -n '"tools_(one|few|many|other)"' cuprum-ui/src/locales/en/project.json cuprum-ui/src/locales/ru/project.json

echo
echo "== calls that rely on count pluralization =="
rg -n -C2 'operations\.meta\.tools|t\(".*tools".*count' cuprum-ui/src

Repository: fixcik/cuprum

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== i18next init configuration ==="
sed -n '10,25p' cuprum-ui/src/i18n/index.ts

echo
echo "=== Check for plural-related config ==="
rg -A 10 'i18n\.use.*init' cuprum-ui/src/i18n/index.ts

echo
echo "=== tools keys in en/project.json ==="
grep -o '"tools[^"]*"' cuprum-ui/src/locales/en/project.json | sort | uniq

echo
echo "=== tools keys in ru/project.json ==="
grep -o '"tools[^"]*"' cuprum-ui/src/locales/ru/project.json | sort | uniq

echo
echo "=== StepCard usage of operations.meta.tools ==="
rg -B5 -A5 'operations\.meta\.tools|meta\.tools' cuprum-ui/src/components/operations/StepCard.tsx

Repository: fixcik/cuprum

Length of output: 1247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full i18n/index.ts file ==="
cat cuprum-ui/src/i18n/index.ts

echo
echo "=== Check for any compatibility or plural settings ==="
rg -r 'compatibilityJSON|pluralSeparator|load.*plural|cldr' cuprum-ui/src/i18n/

echo
echo "=== Check exact en/project.json lines around tools ==="
sed -n '295,310p' cuprum-ui/src/locales/en/project.json

echo
echo "=== Check exact ru/project.json lines around tools ==="
sed -n '295,310p' cuprum-ui/src/locales/ru/project.json

Repository: fixcik/cuprum

Length of output: 748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== en/project.json around tools keys ==="
cat cuprum-ui/src/locales/en/project.json | jq '.operations.meta' 2>/dev/null || sed -n '290,310p' cuprum-ui/src/locales/en/project.json

echo
echo "=== ru/project.json around tools keys ==="
cat cuprum-ui/src/locales/ru/project.json | jq '.operations.meta' 2>/dev/null || sed -n '290,310p' cuprum-ui/src/locales/ru/project.json

echo
echo "=== Check if i18next package.json has plural rules info ==="
rg -A2 -B2 '"i18next"' cuprum-ui/package.json

Repository: fixcik/cuprum

Length of output: 718


Добавьте tools_other в английскую локаль; удалите tools_few и tools_many из неё.

Стандартные CLDR правила pluralization для английского используют только one (для count=1) и other (для 0, 2, 3, ...). В текущем файле отсутствует tools_other, поэтому при count ≠ 1 вместо текста выведется ключ operations.meta.tools. Ключи tools_few и tools_many — это русские правила и в английской локали не используются.

Измените en/project.json с:

"tools_one": "{{count}} drill",
"tools_few": "{{count}} drills",
"tools_many": "{{count}} drills",

На:

"tools_one": "{{count}} drill",
"tools_other": "{{count}} drills",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cuprum-ui/src/locales/en/project.json` around lines 297 - 303, The English
locale file en/project.json has incorrect pluralization rules for the tools key.
English CLDR pluralization only uses `one` (for count=1) and `other` (for all
other counts), but the current implementation includes `tools_few` and
`tools_many` which are Russian pluralization rules and will cause the key name
to be displayed instead of the translation when count is not 1. Replace the
`tools_few` and `tools_many` keys with a single `tools_other` key in the meta
object, keeping the `tools_one` key as is, so the plural form correctly displays
for all non-singular counts.

},
"setup": {
Expand Down
8 changes: 8 additions & 0 deletions cuprum-ui/src/locales/ru/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,14 @@
"hole": "Отверстие",
"tool": "Сверло Ø{{mm}} мм",
"stop": "Стоп"
},
"meta": {
"holes": "отв.",
"tools_one": "{{count}} сверло",
"tools_few": "{{count}} сверла",
"tools_many": "{{count}} свёрл",
"tools_other": "{{count}} сверла",
"side": { "top": "Верх", "bottom": "Низ" }
}
},
"setup": {
Expand Down
4 changes: 3 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
zero/шпиндель), CNC-`MachineProfile` + `ToolLibrary`.
- **Экран «Операции»:** редизайн в две колонки (производственные шаги + история прогонов,
PR #701); live-баннер активной сверловки в левой колонке — пульсирующий статус, счётчик
отверстий, кнопка «Стоп», прогресс-бар (✅ 2026-06-17, PR #703, эпик #697).
отверстий, кнопка «Стоп», прогресс-бар (✅ 2026-06-17, PR #703, эпик #697); строка
параметров операции в карточке шага (drill: отверстия/свёрла/оценка, expose:
сторона/выдержка) из последнего прогона (✅ 2026-06-17, PR #704, #700).

**Текущий фокус:** довести Panel-редактор и выйти на сквозную сверловку на ЧПУ.

Expand Down
Loading