leistungsbilanz-ts/docs/cable-sizing-module.md

243 lines
13 KiB
Markdown

# 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
1. `src/server/index.ts`: one import + one `app.use("/api/cable-sizing", cableSizingRouter)`
line, next to the existing `app.use("/api/projects", ...)` etc.
2. `src/frontend/components/circuit-tree-editor.tsx`: one new `useState` for
the open modal, one new trigger condition (mirroring the existing
`isProtectionTrigger` / `protectionEditorCircuit` pattern 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 against
`designCurrentA = max(operatingCurrentA, protectionDevice.ratedCurrentA)`,
not the load current alone - the standard `In <= Iz` rule (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
`maxVoltageDropPercent` at 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:
```ts
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`
```jsonc
{
"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).