- Seitenansicht (/projects/:projectId/print): tabular cover page, board
overview with supply structure and power sums, one plan per board with a
repeating title block; PDF via the browser print dialog with named @page
rules (A4 portrait cover, A4/A3 landscape plans, page x of y).
- Distribution-board info card (board number, location, fed-from, remark)
in the board edit dialog, persisted through the additively extended
distribution-board.update command (migration 0008, snapshot schema 6,
legacy subtree commands normalized).
- Global document logo upload (PNG/JPEG/SVG, max 2 MB) outside project
revisions and snapshots.
- fed-from stays free text; the overview hierarchy and cumulated sums are
preliminary until the feed link is modeled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
- calculateCableSizing now selects cross-section against
designCurrentA = max(operatingCurrentA, existingProtectionRatedCurrentA)
instead of the load current alone. A breaker only trips at its own rated
current (In), so a cable sized for the actual load could overheat under
sustained current below that threshold - the standard In <= Iz rule.
Voltage-drop calculation still uses the real operating current, only the
capacity check changed. Alerts and the ok-summary now name the breaker as
the limiting factor when it is one.
- Added a manual cable type/cross-section entry in the modal itself:
running a calculation pre-fills these fields as a suggestion, but they
are the single field that actually gets applied, and stay editable -
this restores the direct manual-entry capability the modal replaced when
it took over the cableSummary/cableCrossSection cell click.
- Fixed calculator icon position from a trailing ::after (inconsistent
position depending on cell text length, easy to miss on empty cells) to
a fixed-position ::before, so it is always in the same place regardless
of whether cable data is already filled in.
- No new display for the existing protection device - it already has its
own Schutz column in this app.
Isolated module (mirrors src/external-model/ dependency direction) that
adds an on-demand, DIN VDE 0298-4 referenced cable cross-section
calculator: a click-on-a-circuit input mask for laying method,
insulation, ambient temperature, grouping and voltage-drop limit that
suggests a cross-section for the circuit cableCrossSection/cableLength
fields. Applying a suggestion goes through the existing circuit.update
command (expectedRevision, undo/redo) unchanged - nothing here writes
to circuits directly or touches the revision/command system. See
docs/cable-sizing-module.md for the full rationale, API contract,
verification status and maintenance plan. Proposal, not yet reviewed
by the project owner - see the docs file.
423 existing + 11 new tests pass, build:api/build:web/typecheck:scripts
clean.
Synchronizing a project device pushes its quantity onto every linked
device row, but manualQuantity is not part of the sync snapshot and was
left untouched. A device whose quantity is lower than the row's left the
row at manualQuantity > quantity, violating the snapshot invariant.
The violation surfaced far from its cause: the invariant is only checked
when a full state snapshot is read, and the automatic snapshot runs every
25 revisions. A project could therefore accumulate the broken row
silently and then reject every subsequent command, because the failing
validation rolls back the whole transaction including the revision bump.
Recompute manualQuantity as the synchronized quantity minus the total of
the linked external objects, matching the invariant that
assertCircuitDeviceRowQuantity enforces elsewhere, and reject a
synchronized quantity that falls below that total.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Full-codebase review turned up five real correctness/security bugs and
a dozen smaller inconsistencies; all are fixed here with matching test
coverage:
- BMK uniqueness silently allowed German-umlaut duplicates ("Ä1" vs
"ä1") because the DB's normalized index only folds ASCII case. Added
a shared Unicode-aware pre-check used by every circuit/component
insert and rename path (one of which had no pre-check at all).
- CircuitDeviceRow.simultaneityFactor had no upper bound at the row
level (command model and snapshot/restore schema), unlike every
sibling entity, letting a bad value silently corrupt power totals.
- Grid cell editing silently misread German thousands-separator input
("1.500" parsed as 1.5); "." is now rejected outright with a clear
message instead of guessing.
- The editor's shared command runner (runCommand/applyHistory) had no
re-entrancy guard, so a double click/drop could fire the same
command twice and race a BMK collision or revision conflict. Added a
synchronous ref guard plus isSaving on the buttons that lacked it.
- GET .../next-identifier leaked circuit-numbering state for sections
in other projects (no ownership check, 400 instead of 404). Moved
under /projects/:projectId and scoped it.
Also: added the missing circuits.section_id / circuit_device_rows.
circuit_id indexes (migration 0006), gave FormModal a focus trap /
Escape-to-close / focus restore and rebuilt ProjectSettingsModal on
top of it instead of duplicated markup, removed dead code (3 orphaned
domain model files, an unused persistence helper, a wrapper only used
by its own test), pointed the project page at GET /projects/:id
instead of listing+filtering client-side, closed the gap between the
documented 18 MB CSV limit and the ~17.17 MiB actually enforced, added
missing upper bounds on several free-text fields, filled in nine
missing German labels in the revision timeline, replaced a
key-order-fragile JSON.stringify equality check with a real field
comparison, made an implicit sort-order assumption in three
renumbering helpers explicit, cleared the sidebar's target selection
when it no longer resolves after a tree reload, and fixed
updateGlobalDevice to check-then-write instead of write-then-check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a leveled JSON logger (error/warn/info/verbose/debug, controlled via
LOG_LEVEL) wired into the Express API (access log, error middleware,
crash handlers, memory heartbeat) and the Next.js server (page-request
proxy, instrumentation crash handlers, heartbeat). LOG_LEVEL is exposed
through compose.yaml, and both services now rotate their Docker logs
(json-file, 20m x 10 files) instead of growing unbounded. Intended to
capture long-running diagnostic data for the intermittent 502s seen on
the Docker host.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>