13 KiB
Cable Sizing Module (Proposal)
Status: proposal, not yet reviewed or merged by the project owner. This
document and the accompanying src/cable-sizing/, src/db/schema/cable-sizing-calculations.ts,
src/server/{controllers,routes}/cable-sizing.* and src/frontend/components/cable-sizing-*
files were written by a third party (see git history/authorship) against the
project's own stated direction in docs/spec/06-future-sizing-and-calculations.md
("The app should later support rule-based protection and cable sizing") and
AGENTS.md ("... and later electrical sizing logic"). Nothing here has been
pushed to the project's own repository; it exists as a local branch for
review, testing and discussion.
Why a separate module instead of extending core domain code
AGENTS.md is explicit that critical multi-write commands, the revision/undo
system and the Circuit-First domain model are the supported architecture, and
that changes should be small and reviewable. A cable-sizing calculation is
not a project mutation - it doesn't need expectedRevision, doesn't
belong in the undo/redo stack, and shouldn't grow the circuit.update
command's switch statement or the DTOs it doesn't already have.
So this module is built exactly like the existing src/external-model/
adapter (the Revit/CSV import foundation): a one-way dependency boundary.
src/cable-sizing/domain/ <- pure functions and types, zero imports from
db/, server/ or frontend/
src/db/schema/ <- one new, fully additive table
src/db/repositories/ <- one plain repository (no revision semantics)
src/server/{controllers,routes}/cable-sizing.* <- one new, isolated route
group, mounted with a single app.use() line
src/frontend/components/cable-sizing-* <- one new modal + one small
API client, following the existing
FormModal / CircuitProtectionModal pattern
Nothing outside these files imports from them except the two required
one-line hooks described below. If this module is rejected or needs to move
out again, removing it is a matter of deleting these files, the two
one-line hooks, and running a DROP TABLE migration - it never touches
circuits, project_revisions or any command/history table.
The two required hooks into existing code
src/server/index.ts: one import + oneapp.use("/api/cable-sizing", cableSizingRouter)line, next to the existingapp.use("/api/projects", ...)etc.src/frontend/components/circuit-tree-editor.tsx: one newuseStatefor the open modal, one new trigger condition (mirroring the existingisProtectionTrigger/protectionEditorCircuitpattern almost exactly), and one new conditionally-rendered<CableSizingModal />alongside the existing<CircuitProtectionModal />. See the diff for the exact lines.
No changes to circuit-project-command.model.ts, project-command.service.ts,
any Zod command schema, any migration for circuits/project_revisions, or
any existing test.
Coupling to the grid UI (the one fragile point)
Everything described above - the calculation, the audit table, applying a
result via circuit.update - depends only on stable, explicitly documented
domain fields (cableType/cableCrossSection/cableLength/
circuitTotalPower/voltage/protectionDevice.ratedCurrentA), the same
ones AGENTS.md already treats as protected ("Protection and cable data
belong to the circuit"). None of that breaks if the grid UI changes.
The one place that is coupled to a UI implementation detail: the click
trigger in circuit-tree-editor.tsx matches grid column keys
(cableSummary and cableCrossSection, defined in
circuit-grid-model.ts) to decide where to show the calculator icon.
Column visibility and order in this app are a per-browser user preference,
not fixed - so this is checked against both known cable-related columns to
reduce (not eliminate) the chance a user's personal column layout hides the
trigger entirely. If a future column rename removes both keys, the failure
mode is purely cosmetic: the calculator icon/click stops appearing on that
cell, nothing crashes, no data is affected, and the rest of the app
(including manual cable-field editing) is completely unaffected - the
module's actual logic and persistence never depend on this column key.
Feature overview
- Calculate a recommendation: laying method, insulation, conductor material, ambient temperature, grouping, cos phi and max voltage drop - same VDE 0298-4 reference-method model as the sibling Kabelliste tool (see Verification status below).
- Breaker-aware sizing: if the circuit already has a protection device
(
circuit.protectionDevice), the cross-section is selected againstdesignCurrentA = max(operatingCurrentA, protectionDevice.ratedCurrentA), not the load current alone - the standardIn <= Izrule (a breaker only trips at its own rated current, so a cable sized for the actual load alone could overheat under sustained current below that threshold). If no cross-section can cover an oversized breaker, that is reported as the limiting factor by name, not a generic capacity error. - Maximum length for the voltage-drop limit: each cross-section option
also reports the longest single run that still meets the requested
maxVoltageDropPercentat the given load - the inverse of the voltage-drop check, shown for the recommended cross-section. - Practical minimum for socket circuits:
single_phase-category circuits are raised to at least 2.5 mm² if the calculation alone would recommend less - see the dedicated note under Verification status. Never lowers a calculation that already needs more. - Manual entry, always available: the modal has a plain text Kabeltyp/
Querschnitt field. Running a calculation pre-fills it as a suggestion,
but the field is the single source of truth for what gets applied, and
stays freely editable - satisfying
docs/spec/06-future-sizing-and-calculations.md's explicit requirement that "users must remain able to manually override suggestions" without requiring a calculation to have been run first. A non-blocking warning appears if the typed value is not a standard cross-section, is smaller than the last calculation, or is below the practical minimum above - informational only, never forced, matching the same spec doc's "shown as a warning or status indicator, not as an automatic forced change."
How a suggestion or manual entry is applied
The module never writes to circuits directly. The modal's "Übernehmen"
button calls the existing, unmodified frontend helper with whatever is
currently in the Kabeltyp/Querschnitt fields, calculated or hand-typed:
updateCircuitById(projectId, expectedRevision, circuitId, {
cableCrossSection: "4 mm²",
cableType: "NYM-J 3x2.5",
cableLength: 30,
});
which is the same circuit.update command the grid already uses for manual
cell edits. This means:
- optimistic concurrency (
expectedRevision) is respected automatically - the change appears in the project's revision history and is undoable/ redoable exactly like a manual edit
- nothing is ever written automatically - calculating only fills the modal's own fields, applying is always an explicit, separate click
API contract
POST /api/cable-sizing/calculate
{
"phase": 1, // 1 | 3
"mode": "power", // "power" | "current"
"powerKw": 4.2, // circuit.circuitTotalPower, when mode="power"
"cosPhi": 1,
"voltage": 230, // circuit.voltage (already project-derived, read-only)
"lengthM": 23.5, // circuit.cableLength
"layingMethod": "C", // DIN VDE 0298-4 reference method, A1|A2|B1|B2|C|D1|D2|E|F|G
"conductorMaterial": "copper", // "copper" | "aluminum"
"insulation": "pvc", // "pvc" | "xlpe"
"ambientTemperatureC": 30,
"groupingCircuits": 1,
"maxVoltageDropPercent": 3,
"harmonicNeutralLoad": "none", // "none" | "15to33Percent" | "over33Percent", three-phase only
"existingProtectionRatedCurrentA": 16, // optional, from circuit.protectionDevice
"circuitCategory": "single_phase", // optional, from the circuit's section.category -
// enables the practical-minimum convention below
"context": { "projectId": "...", "circuitId": "...", "equipmentIdentifier": "-1F1.1" }
}
Response: { calculationId, result: CableSizingResult, alerts: CableSizingAlert[] }
(see src/cable-sizing/domain/cable-sizing-calculation.ts for the exact
shape). GET /api/cable-sizing/laying-methods and .../insulation-materials
expose the pick-list metadata (including which combinations are verified) so
a future non-modal UI could build its own form without hard-coding the enum.
Verification status - important
The reference current-carrying-capacity tables are ported from a sibling
project's Kabelliste module, which was itself only verified against ~8
public sources for six of the ten DIN VDE 0298-4 reference laying
methods (A1, B2, C, E, D1, D2) and PVC insulation only. The other four
methods (A2, B1, F, G) and XLPE/VPE insulation could not be verified without
contradiction across sources during that earlier work, so
calculateCableSizing deliberately returns dataVerified: false and no
numeric recommendation for those combinations, rather than a guessed value.
The UI surfaces this as a plain critical alert. Anyone with access to the
actual norm text can extend LAYING_METHOD_VERIFIED and
CURRENT_CAPACITY_A in src/cable-sizing/domain/cable-sizing-calculation.ts
once the missing tables are confirmed.
The protection-coordination check (existingProtectionRatedCurrentA vs. the
recommended cross-section's corrected capacity) is a simplified In <= Iz check only - it is not a full IEC 60364-4-43 overload (I2 <= 1.45 x Iz) or short-circuit withstand check.
PRACTICAL_MINIMUM_CROSS_SECTION_MM2 (2.5 mm² for single_phase) is
not a verified norm value at all - it is a named planning convention,
sourced directly from this project's own
docs/spec/06-future-sizing-and-calculations.md ("Standard Single-Phase
Circuits ... usually use ... cable cross-section: 2.5 mm²"), applied as a
floor on top of the calculated recommendation. It intentionally only
covers the one category and one convention that document already states;
it is not a general substitute for norm-compliant calculation.
Relationship to the project's own future-sizing spec
docs/spec/06-future-sizing-and-calculations.md separately describes simple
category-based defaults (e.g. lighting circuits -> 10 A / 1.5 mm²) as
"common planning defaults, not a replacement for full norm-compliant
calculation". This module is the latter: an on-demand, norm-referenced
calculation for one circuit at a time, not a bulk default-filling tool. The
two are complementary and could later be wired together (e.g. the category
defaults pre-fill this module's laying-method/insulation fields), but that
integration is out of scope here.
The spec's isPublicBuilding halogen-free rule and the Control Requirement
(DALI/KNX/core-count) field are not implemented by this module - both
concern cable type selection, not cross-section sizing, and the latter
isn't in the current schema yet. They're natural follow-ups once this module
is reviewed.
Maintenance plan
The src/cable-sizing/domain/ folder has zero imports from the rest of the
app by design (see the dependency-direction note above), so it can be
extracted into an independently versioned/published package later (e.g. a
private npm package or a git subtree) without touching anything outside the
five files/folders listed above - "separately maintained but part of the
app for now", per the intent of this proposal.
Local testing
Deployed and manually exercised end-to-end on an internal test host at
http://192.168.0.133:3220 (own docker deployment, own subnet/ports to
avoid clashing with ~50 other containers on that host - see the two
commits marked "Local-only" in this branch, which are not part of this
proposal and should not be carried over if it is ever proposed upstream).
Exercised against seeded real project/circuit data created through the
actual command API (not direct DB writes), including a deliberately long
(85 m) circuit to trigger the voltage-drop-critical path.
npm test, npm run build:api, npm run build:web and
npm run typecheck:scripts all pass with this module included at every
commit in this branch.
This branch (feature/cable-sizing-module) is pushed to a mirror of this
repository under this homelab's own Forgejo instance, not to
git.jappel.io - no write access to the upstream repository was available
or used. See the branch's commit history for the incremental history of
this module, including fixes made during manual testing (trigger column,
icon rendering, breaker-aware sizing).