cable-sizing: max length, socket minimum, manual-entry warning, SVG icon

- Cross-section rows now include maxLengthForVoltageDropM, the inverse of
  the voltage-drop formula: the longest single run that stays within the
  requested max voltage drop at the given load/cosPhi/phase. Shown as a KPI
  and in the ok-summary alert.
- Added PRACTICAL_MINIMUM_CROSS_SECTION_MM2 (2.5 mm² for single_phase
  circuits) - a planning convention already named in this projects own
  docs/spec/06-future-sizing-and-calculations.md, not a thermal/
  voltage-drop requirement. Only ever raises a calculated recommendation,
  never lowers one that already needs more. circuitCategory is looked up
  from the circuits section and passed through from the editor.
- The manual cross-section field now warns (not blocks - manual override
  must stay possible per the same spec doc) when the typed value is not a
  standard cross-section, is smaller than the last calculation, or is
  below the practical minimum for single_phase circuits.
- Modal title now includes the circuit displayName next to the equipment
  identifier.
- Replaced the calculator emoji trigger icon with an inline SVG - the emoji
  did not render in at least one tested environment (missing font glyph),
  SVG has no such dependency.
This commit is contained in:
Grovy311 2026-08-07 18:13:27 +02:00
parent 8562d6eb83
commit cfb76a3754
6 changed files with 210 additions and 6 deletions

View file

@ -134,7 +134,7 @@ describe("buildCableSizingAlerts", () => {
assert.ok(alerts[0].text.includes("keine geprüften"));
});
it("returns a single ok alert for a clean, unremarkable case", () => {
it("returns an ok alert plus a max-length info alert for a clean, unremarkable case", () => {
const input: CableSizingInput = {
...BASE_INPUT,
mode: "current",
@ -144,7 +144,66 @@ describe("buildCableSizingAlerts", () => {
};
const result = calculateCableSizing(input);
const alerts = buildCableSizingAlerts(input, result);
assert.equal(alerts.length, 1);
assert.equal(alerts.length, 2);
assert.equal(alerts[0].kind, "ok");
assert.equal(alerts[1].kind, "info");
assert.ok(alerts[1].text.includes("Maximale Länge"));
});
});
describe("maxLengthForVoltageDropM", () => {
it("is the inverse of the voltage-drop formula: recalculating at that length gives back the limit", () => {
const result = calculateCableSizing(BASE_INPUT);
const recommendedRow = result.rows.find((row) => row.recommended);
assert.ok(recommendedRow?.maxLengthForVoltageDropM != null);
const atMaxLength = calculateCableSizing({
...BASE_INPUT,
lengthM: recommendedRow!.maxLengthForVoltageDropM!,
});
const rowAtSameCrossSection = atMaxLength.rows.find(
(row) => row.crossSectionMm2 === recommendedRow!.crossSectionMm2
);
assert.ok(
Math.abs(rowAtSameCrossSection!.voltageDropPercent! - BASE_INPUT.maxVoltageDropPercent) <
0.01
);
});
it("is null when there is no current flowing (division by zero guard)", () => {
const result = calculateCableSizing({ ...BASE_INPUT, mode: "current", currentA: 0, powerKw: undefined });
assert.ok(result.rows.every((row) => row.maxLengthForVoltageDropM === null));
});
});
describe("practical minimum cross-section for single_phase circuits", () => {
it("raises a smaller calculated recommendation to 2.5 mm² for single_phase circuits", () => {
// 1 A load at 30 m would normally recommend 1.5 mm² by calculation alone.
const smallLoad: CableSizingInput = { ...BASE_INPUT, mode: "current", currentA: 1, powerKw: undefined };
const withoutCategory = calculateCableSizing(smallLoad);
const withCategory = calculateCableSizing({ ...smallLoad, circuitCategory: "single_phase" });
assert.equal(withoutCategory.recommendedCrossSectionMm2, 1.5);
assert.equal(withoutCategory.practicalMinimumApplied, false);
assert.equal(withCategory.recommendedCrossSectionMm2, 2.5);
assert.equal(withCategory.practicalMinimumApplied, true);
});
it("never lowers a recommendation that already needs more than the practical minimum", () => {
const result = calculateCableSizing({ ...BASE_INPUT, circuitCategory: "single_phase" });
assert.equal(result.recommendedCrossSectionMm2, 4);
assert.equal(result.practicalMinimumApplied, false);
});
it("does not apply to lighting or three_phase categories", () => {
const smallLoad: CableSizingInput = { ...BASE_INPUT, mode: "current", currentA: 1, powerKw: undefined };
assert.equal(
calculateCableSizing({ ...smallLoad, circuitCategory: "lighting" }).recommendedCrossSectionMm2,
1.5
);
assert.equal(
calculateCableSizing({ ...smallLoad, circuitCategory: "three_phase" }).recommendedCrossSectionMm2,
1.5
);
});
});