# AGENTS.md ## Project Goal Maintain and extend a spreadsheet-like electrical distribution board circuit list editor. The editor is used for electrical planning in execution design. It must support circuits, device rows, project devices, drag-and-drop restructuring, stable equipment identifiers and later electrical sizing logic. ## Current Supported Architecture - Frontend: Next.js App Router under `src/app` with reusable editor modules under `src/frontend`. - API: Express composition starts in `src/server/index.ts`. - Domain rules: `src/domain`. - Persistence: SQLite/Drizzle schemas, repositories and migrations under `src/db`. - Primary editor route: `src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx`. - Primary editor component: `src/frontend/components/circuit-tree-editor.tsx`. - Grid ownership, projection, insertion and safety rules live in the pure `src/frontend/components/circuit-grid-*.ts` modules. - Critical multi-write commands use injected transaction repositories with real SQLite commit/rollback tests. - All supported runtime project-command stores, including full snapshot restoration, share `src/db/repositories/project-command-transaction.persistence.ts` for the atomic domain-write, revision and history transition boundary. Its applied forward-command variant preserves derived CircuitDeviceRow override metadata. - Low-level `appendProjectRevision` persistence is adapter-internal and tested directly; do not reintroduce a standalone runtime revision repository. - Runtime domain services receive narrow reader/store dependencies explicitly; concrete SQLite repositories are instantiated only under `src/server/composition`. - General application repositories also require an explicit `AppDatabase`; `src/server/composition/application-repositories.ts` owns their runtime instances, and controllers never import the global SQLite client. - General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard repositories expose only active reads. Runtime writes belong in typed command repositories; direct integration fixtures belong under `tests/support`. - Authentication: session-cookie login with two fixed roles (`admin`, `user`), no self-signup. `src/server/auth/*` (password hashing, sessions, Express middleware), `src/domain/services/user.service.ts`, `src/db/repositories/user.repository.ts`. Every `/api/*` route requires a signed-in user (`src/server/index.ts`); `/api/users/*` additionally requires `admin`. Frontend gate: `AuthProvider` + `LoginGate` (`src/app/layout.tsx`). Every unsafe API request also requires the explicit `X-Leistungsbilanz-Request: 1` application header, including over plain HTTP. Account settings are available to every signed-in user; user administration remains admin-only. See `docs/authentication.md` for the full picture. The supported runtime model is Circuit-First. The former Consumer UI, API, tables and upgrade tooling are removed; do not reintroduce them. See `docs/current-architecture.md` for the complete module and request flow. ## Critical Domain Rules - A circuit is not the same thing as one device row. - A circuit contains zero, one or multiple device rows. - A single-device circuit may be displayed as one compact row. - A multi-device circuit must be displayed with a circuit summary row and indented device rows. - Equipment identifiers belong to circuits, not to every device row. - Device rows inside a circuit do not have their own equipment identifiers. - Protection and cable data belong to the circuit, not to individual devices. - Device-level values include quantity, power per unit, simultaneity factor, cosPhi, room data, cost group and category. - Circuit total power is the sum of all device rows in that circuit plus the read-only loads of logically linked downstream boards on that feeder circuit. - Existing equipment identifiers must never be changed automatically. - Renumbering is always an explicit user action. ## Existing Codebase Rule There is an existing codebase. Do not blindly continue old assumptions. Do not blindly delete the codebase. First audit current code against the specification. Keep working parts if they match the target model. Refactor or replace parts that conflict with the target model. Before large changes, summarize: - what is kept - what is changed - what is removed - why ## Implementation Discipline Work in phases. Do not implement unrelated future features while working on a phase. Before coding, briefly state: - files to change - data model impact - UI impact - risks Prefer small, reviewable changes. Do not rewrite the whole app unless the current architecture blocks the required domain model. ## Naming Use English names in code. Use clear domain names: - `ProjectDevice` - `DistributionBoard` - `CircuitSection` - `Circuit` - `CircuitDeviceRow` - `equipmentIdentifier` - `displayName` - `phaseType` - `quantity` - `powerPerUnit` - `simultaneityFactor` - `cosPhi` - `rowTotalPower` - `circuitTotalPower` Avoid ambiguous names like: - `item` - `thing` - `entry` - `rowData` Use them only for local UI variables where the context is obvious. ## Numbering Rules Default sections: - Lighting: prefix `-1F` - Single-phase circuits: prefix `-2F` - Three-phase circuits: prefix `-3F` New circuit identifier: - highest existing number in the section + 1 Do not fill gaps automatically. Do not renumber after insert, delete, move, sort or drag-and-drop. Renumber only when the user explicitly triggers "Renumber section". ## UI Rules The circuit list table should behave like a spreadsheet. User-facing frontend text must be German unless a domain-standard technical term is intentionally retained. Cells show static text by default. Inline edit starts by: - double click - Enter - typing - F2 Keyboard behavior: - Enter confirms - Escape cancels - Tab / Shift+Tab navigates editable cells - Arrow keys navigate cells when not editing - Alt+ArrowDown on a GZF cell opens the AMEV table (`GzfPickerModal`); the selected GZF cell also shows a small table-icon button. The picked value is committed like a typed edit. Support Ctrl+Plus and Ctrl+Shift+Plus for insertion. Editor actions use `IconButton` (`src/frontend/components/icon-button.tsx`, `lucide-react` icons). The German action text is always the accessible name and tooltip; the toolbar shows icon plus text, group headers and rows show icons only, separated into logical blocks with `IconButtonSeparator`. Destructive actions use `variant="danger"` (trash icon for circuits and groups, `ListX` for removing a device row). Column headers show the label (click to sort) with a funnel icon for the value filter; an active filter is highlighted and shows its value count. Header labels break only between words and put a trailing unit such as `[kW]` on a second line. The project overview uses the same rule: row actions are icon-only `IconButton`s, card-header and panel actions show icon plus text, navigation uses `IconLink` (`src/frontend/components/icon-link.tsx`). Page-header buttons stay Bootstrap `btn` with a lucide icon (`btn-with-icon`). List cards filter through a collapsible PM-style filter bar (`CollapsibleFilterPanel`, `MultiSelectDropdown`; logic in `src/frontend/utils/overview-filters.ts`): only the bar collapses, its state is kept per card in `localStorage`, a collapsed active filter shows "Filter aktiv". ## Bootstrap / Styling Rule Bootstrap may be used for the general application UI, such as navigation, page layout, buttons, forms, cards, alerts and modals. Cards follow the PM app look: 1px border all around, 16px radius and the soft shadow defined on `.card` in `globals.css` (own dark-mode value). The `.card` rule maps Bootstrap's `shadow-sm` to the same shadow; do not add other `shadow-*` classes or a colored top border to cards. Do not use Bootstrap as the core table/grid framework for the circuit list editor. The circuit list editor must remain a custom spreadsheet-like component with controlled cell selection, inline edit mode, row grouping, drag-and-drop indicators and keyboard behavior. Avoid permanent Bootstrap form controls inside table cells. Table cells should show static text by default and switch to inputs only while editing. ## Browser Runtime The web UI is also opened over plain HTTP from other machines in the LAN, which is not a secure context. Do not call secure-context-only browser APIs such as `crypto.randomUUID()` directly; create client-side ids with `createClientId()` from `src/frontend/utils/client-id.ts`. ## Drag-and-Drop Rules Dragging from the circuit identifier / circuit handle moves the whole circuit. Dragging from the device area moves device rows. Project devices can be dragged from a sidebar into the circuit list. Drop onto a free placeholder creates a new circuit. Drop onto an existing circuit adds the device to that circuit. Moving a device recalculates affected circuit totals. Moving rows never renumbers circuits automatically. Show clear visual drop indicators. Reject invalid drop targets or require confirmation. ## Sorting and Filtering Filtering should work through column headers. Sorting should work through column headers. Sorting moves complete circuits as blocks. Sorting must not split device rows away from their circuit. Sorting does not renumber. The user may explicitly renumber after sorting. ## Linked Project Devices Circuit device rows may be linked to project devices. `displayName` is copied on insert but not synchronized automatically. When a project device changes, show affected linked rows and let the user choose which fields to sync. Never silently overwrite local changes. Allow disconnecting linked rows from project devices. ## Manual Rows Manual rows are allowed and common. Manual rows can later be saved as project devices. After saving, the row becomes linked to the new project device. ## Undo / Redo The editor reads undo/redo eligibility from the project-wide server history on initial load and after every tree reload. Undo/redo therefore remains available after a page refresh or application restart. All currently supported Circuit and CircuitDeviceRow writes execute persistent project commands. Applying a sorted view across multiple sections is one atomic `circuit.reorder-sections` command and one undo step. Explicit renumbering uses the collision-safe `circuit.renumber-section` command and is never triggered implicitly. Immutable revision metadata is available through the paginated `GET /api/projects/:projectId/history/revisions` endpoint; it does not expose stored command payloads. Named logical snapshots can be created and listed through project-scoped API endpoints. Their schema-versioned payload contains the complete supported project runtime state and a SHA-256, excludes global data outside the project and does not change the project revision or undo/redo stacks. Restoring a server-stored snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new `restore` revision with a complete inverse command. Restore can therefore be undone and redone after a restart. Current snapshot schema version 10 stores the Linear-XLSX configuration and external state with Worksharing-ID identity. Empty versions 1 to 9 are upgraded; versions containing legacy CSV configuration or external CSV state are rejected with a German migration error. Version 9 additionally stores `feedingDistributionBoardId` and `feedingCircuitId` on distribution boards; versions 1 to 8 are upgraded with both references set to `null`. Version 8 stores the optional net room area `rooms[].areaM2` (see `docs/building-structure.md`); versions 1 to 7 are upgraded with `areaM2 = null`. Version 7 stores the project buildings and `floors[].buildingId`; versions 1 to 6 are upgraded with no buildings and `buildingId = null`. Version 6 stores the distribution-board info fields `boardNumber`, `location`, `fedFrom` and `remark` and the project cover fields `siteAddress`, `workStage`, `projectLead` and `projectEditor`; all older supported versions are upgraded with `null` for these fields. Stored named and automatic snapshots of versions 1 to 9 remain restorable when they contain no legacy CSV state; they are upgraded while parsing. Version 5 stores the manual quantity share of every CircuitDeviceRow. Version 4 snapshots are upgraded with `manualQuantity = quantity`; they already contain the complete confirmed external-model source, import batches, room mappings and objects. Version 3 snapshots are upgraded with an empty external-model state; version 2 snapshots are additionally upgraded with an empty external CSV configuration. Supported baseline version 1 snapshots are additionally upgraded with `isPublicBuilding = false`; pre-baseline snapshots remain unsupported. All supported versions contain circuit-group identity, distribution-board components and the separate circuit/component protection records. Portable duplicate imports remap component ids and protection-owner references together with the existing project graph. R3 replaced the runtime configuration and persistence foundation with Linear XLSX and exact case-sensitive Worksharing-ID identity. Migration `0014` drops the old development-only external CSV data after resetting linked row totals to their manual share. Snapshot schema 10, restore and project transfer use the new state; non-empty legacy CSV snapshots and legacy CSV history entries are rejected with a German error. The backend CSV parser and routes are removed. R4 adds `/projects/:projectId/revit`, its revisioned XLSX settings and server-side sample detection, and removes the old project-page CSV modal. See `docs/spec/revit-excel-integration-requirements.md` and `docs/spec/revit-excel-phase-14-plan.md`. The target configuration has no independent version counter; `expectedRevision` invalidates stale plans, and each import batch stores a configuration copy. Only the newest import batch retains workbook bytes and the full matrix; older batches retain date, filename, SHA-256 and object count. Phase 14.3 migration `0005` adds `CircuitDeviceRow.manualQuantity`, backfills existing rows from `quantity` and keeps `quantity` as the materialized total. Normal manual row inserts and quantity edits update both values together. `external-object.update-row-assignment` atomically assigns external objects to existing rows, moves them between existing rows or detaches them. It preserves the manual quantity, validates the materialized total against every target link, rejects project/board/category/room/selection-marker mixing and requires explicitly listed confirmations for other planning-value conflicts. Its exact inverse supports persistent Undo/Redo, and detach never deletes the row. `external-object.assign-to-new-row` atomically inserts unassigned external objects as one external-only row (`manualQuantity = 0`) into an existing circuit. Its history-only inverse removes the row only while its complete snapshot and exact object-link set remain unchanged. Ordinary detach continues to preserve the row. `external-object.assign-to-new-circuit` atomically inserts a preplanned circuit with stable BMK/UUIDs, derived voltage, one external-only row, required default protection and its external object links. Its history-only inverse requires the complete unchanged circuit snapshot and exact link set. Redo never recalculates the BMK. All three external assignment stores share `external-object-assignment.persistence.ts` for compatibility and link safety. The current read-only `GET /api/projects/:projectId/circuit-lists/:circuitListId/external-objects` projection resolves the board through the owned CircuitList and returns compact legacy external objects. The target Revit page will load one compact project list and filter/sort it client-side in a virtualized table, without server pagination or batch blobs in the DTO. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. The project page exposes persistent project-wide Undo/Redo in the header of an initially collapsed German snapshot/timeline UI, with explicit restore confirmation and cursor-based loading of older revision metadata. Insertions and generated move targets use client-generated stable UUIDs, and undo restores the same ids from complete server snapshots. The tree response supplies the optimistic `currentRevision`; obsolete direct field PATCH, structure POST, move, reorder, renumber, identifier-restore, Circuit and CircuitDeviceRow DELETE routes are removed. CircuitDeviceRow moves between existing circuits and moves that create one new placeholder target circuit are persisted. The latter stores the complete empty target snapshot so undo can restore the rows and remove only the unchanged generated circuit. Complete in-section Circuit reorders are persisted separately and change sort positions without changing equipment identifiers. Explicit complete-section renumbering is persisted through a separate collision-safe command and is never triggered by sorting or moving. Project-device synchronization, disconnect and reconnect are persisted as one atomic multi-row command with complete expected/target row snapshots, including link and override metadata. Canonical ProjectDevice field updates are also persisted and never synchronize linked rows implicitly. ProjectDevice insertion/deletion preserves stable device ids. Deletion captures complete disconnected snapshots of linked rows so undo can restore only rows that have remained unchanged. ProjectDevice create, update, delete and global-to-project copy API/UI paths use these persistent commands and track the returned project revision. ProjectDevice synchronization/disconnect API and UI paths do the same; their undo action uses the project-wide history endpoint. Project settings use the persistent `project.update-settings` command. Project metadata, the public-building classification, both voltage defaults and the enabled distribution-board supply types change in one revision and Undo/Redo restores them together; the project PUT route requires `expectedRevision`. Schema version 3 additionally carries the print cover fields `siteAddress`, `workStage`, `projectLead` and `projectEditor` (migration `0008`); version 2 (without them) and baseline version 1 stay executable and keep the current cover values. The PUT route accepts the cover fields optionally. The public-building flag is the future sizing input for requiring halogen-free cables and lines. The system catalog is `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board cannot be disabled. Distribution-board setup uses `distribution-board.insert` schema version 1 with a complete stable snapshot of the board, circuit list, three default groups, main switch `-Q0` and surge protective device `-FA`. Its inverse removes only the same unchanged and still-empty structure; the POST route requires `expectedRevision` and returns the updated history state. Complete populated distribution-board copying and deletion use `distribution-board.insert-subtree` and `distribution-board.delete-subtree`. Their exact snapshots include the board, circuit list, groups, circuits, device rows, components and all one-to-one protection data. Copy remaps all owning ids but preserves project-device and room links. Both operations are atomic persistent commands with restart-safe Undo/Redo; deletion requires an explicit UI warning. Mutable group-protection and auxiliary distribution-board components use `distribution-board-component.insert` and `distribution-board-component.delete`; edits and footer reordering use `distribution-board-component.update`. Their complete expected/target snapshots include the optional one-to-one protection-device state and reject stale data. Updates preserve id, ownership, role and placement. Fixed main-switch and surge-protection header roles are excluded from these general component commands. Empty circuit-group creation, display-name updates and deletion use `circuit-group.insert`, `circuit-group.update` and `circuit-group.delete`. Their snapshots keep category, positive group number and derived prefix consistent. General deletion rejects groups containing circuits or group components; populated deletion remains a separate confirmed command. `circuit-group.reorder` requires a complete expected/target sort assignment for every group in one circuit list. It changes only sort positions and never renumbers groups, prefixes or circuits. Explicit group-number changes use `circuit-group.renumber`. The command carries the complete expected/target group, circuit-BMK and optional group-component BMK plan, applies swaps through collision-safe temporary values and preserves circuit suffixes. Undo/Redo uses the exact inverse plan. Cross-group circuit moves use `circuit.move-group` and are limited to distinct groups of the same category. The stored target BMK is highest target suffix plus one at planning time and is never recalculated for Redo. Only circuit group, BMK and sort order change; device rows and circuit protection retain the stable circuit id. Confirmed populated-group deletion uses `circuit-group.delete-subtree`/`circuit-group.restore-subtree`. The exact snapshot contains the group, optional component protections, complete circuits, circuit protections and all device-row link/override metadata. Delete re-captures and compares the subtree before cascading; restore preserves every UUID in foreign-key-safe order. The project-wide circuit-tree read model exposes fixed `headerComponents`, group-owned `components`, `footerComponents`, optional group category/number and optional one-to-one protection DTOs for circuits and group components. These are the only supported circuit-protection source; no flat compatibility fields remain. It resolves logical board feeds once for the complete project and adds each downstream board's demand after its own board simultaneity factor to the chosen feeding circuit as a read-only virtual row. Recursive chains and several boards on one circuit are supported. A board-only link affects hierarchy but adds no load until a feeding circuit is selected. The distribution-board dialog can create one empty feeder circuit and link it in the same `distribution-board.create-feeder-circuit` project command. Its history inverse restores the complete previous board update before removing only the unchanged generated circuit; Redo reuses its UUID and BMK. No CircuitDeviceRow is created. The three-phase group is suggested, the optional circuit display name stays empty by default, and protection remains open for planner sizing. A circuit with exactly one downstream board and no device rows is rendered as one compact row using the live board number/name; several downstream boards or additional devices retain summary and child rows. Editing the name or remark of an empty reserve circuit changes the circuit itself and does not implicitly create a device row. Distribution-board floor assignment and a project-enabled supply type use `distribution-board.update`; both values are snapshot/export fields and one persistent undo step. The same command uses schema version 2 for the logical `feedingDistributionBoardId` and optional matching `feedingCircuitId` together with the existing fields. Version-1 history commands remain executable and do not change logical feed references. The command rejects self-links, cycles and circuits from another board. The board info-card fields `boardNumber`, `location`, `fedFrom` and `remark` (migration `0007`). They are nullable, trimmed planner values; the PUT route only includes submitted info fields in the command and its inverse. An empty board with info values is no longer an unchanged setup, so history deletion of it is rejected like a changed simultaneity factor. Populated-subtree commands stored before these fields existed are normalized to `null` values (`normalizeDistributionBoardSubtreeSnapshot`). `fedFrom` remains free text for external feeds and notes; project-board feeds use the two UUID references. Deleting a referenced board or feeding circuit is blocked in every direct, subtree and history-only removal path. Board copies retain their incoming feed, and project duplication remaps both references. These fields are intended to be prefillable from Revit parameters later, but only through the Phase 14 rules (preview, explicit confirmation, no silent overwrite). Floor and room setup uses persistent insert, update and delete commands with complete stable snapshots. Room command schema version 2 stores the optional, finite, non-negative net room area `areaM2`; persisted version-1 commands are upgraded with `areaM2 = null` and exact record comparison prevents them from silently overwriting a newer area. Room areas are maintained manually, shown with two decimals and summarized per floor and building with a separate missing count. Revit import and W/m² use remain follow-up work. Command inverses remove or restore only unchanged records and reject floors with assigned rooms/distribution boards or rooms referenced by device rows. All write routes require `expectedRevision` and return the updated history state. Required operations: - insert circuit - insert device - delete circuit - delete device - move circuit - move device - multi-row move - renumber section - edit cell value - edit equipment identifier - synchronization changes ## Print View (Seitenansicht) - Route `src/app/projects/[projectId]/print/page.tsx`, opened from the project page button "Seitenansicht". Read-only: it only uses existing GET endpoints. - Pure read model `src/frontend/print/print-document-model.ts` (tested in `tests/print-document-model.test.ts`); React sheets in `src/frontend/print/print-document.tsx`; styles scoped to `.print-view` in `src/frontend/print/print-document.css`. - Pages: cover (A4 portrait, tabular), board overview with structure and power sums, one plan per board (A4/A3 landscape) whose title block sits in `` so it repeats on printed pages. PDF export uses the browser print dialog; page size, margins and "Seite x von y" come from injected named `@page` rules (`lb-portrait`, `lb-landscape`). - The board hierarchy follows logical board ids. The overview shows power before board GZF and demand including linked downstream boards; it does not add a second cumulative column. Board plans render downstream-board loads as read-only rows. `fedFrom` groups only external root feeds. - Colors/fonts follow the kbp corporate design (`--pd-*` variables in `print-document.css`). Empty cover values are shown as "[ergänzen]"; the Planstände editor column shows the project's current `projectEditor` because snapshots do not record an editor. - The document logo and the office profile (office name/address on the cover) are global office branding: tables `document_branding_assets` (migration `0007`) and `document_branding_settings` (migration `0008`), endpoints `GET|PUT|DELETE /api/document-branding/logo`, `PUT /api/document-branding/office` and `GET /api/document-branding`. It is intentionally outside project revisions, snapshots and transfer. Uploads are PNG/JPEG/SVG up to 2 MB with a content signature check; the logo is served with `nosniff` and a restrictive CSP. ## Revit / Linear Excel External Model Work Phase 14 is active and follows `docs/spec/revit-excel-integration-requirements.md` plus `docs/spec/revit-excel-phase-14-plan.md`. The old CSV requirements and phases 14.0–14.6 are superseded. Dynamo, Revit API and JSON transport remain excluded. Critical target rules: - Exchange uses only `.xlsx` files produced and consumed by the Linear plugin. - Revit integration remains optional. Import never creates a CircuitDeviceRow, Circuit or equipment identifier. - Worksharing-ID is the external key and is compared exactly and case-sensitively. It stays separate from internal UUIDs and circuit BMKs. IfcGUID is optional redundancy, never a key, and duplicate values are only a warning. - A row without Worksharing-ID is suspicious only when `Familie und Typ` is populated. Show a warning and skip it; do not block. Other ID-less rows, including the sample summary in column B, are skipped silently. - Source, planning and override values remain separate. Existing ProjectDevices remain canonical and are never silently changed. - Multi-socket objects remain indivisible. Row quantity stays `manualQuantity + sum(effectiveQuantity)`. - The project configuration has no independent version counter. Preview and apply use `expectedRevision`; import batches store a configuration copy. - Apply retransmits and parses the workbook. Undo/Redo never reads Excel; they apply only stored before/after states. - Only the newest import batch retains original bytes and the full matrix; older batches retain date, filename, SHA-256 and object count. - The Revit object table loads one compact list, filters/sorts in the client and renders it virtually; do not add server pagination. - Linked objects always follow their row's distribution board. Changing a room's default board affects only unassigned objects and is not blocked. - Deleting a row, circuit or group warns that X Revit objects become unassigned, detaches them in the same command and restores links on Undo. Only deletion of referenced rooms and distribution boards is blocked. - Missing optional return columns for designation, power, voltage or current produce warnings and are skipped. Only missing circuit columns for the selected combined/separate mode block export. - Missing GZF defaults visibly to 1 without separate confirmation. - The corrected R2 return workbook was successfully imported into Linear on 2026-09-30; R3 and R4 are complete and R5 is the next package. - All confirmed writes use typed project commands with `expectedRevision`, complete inverse data and the shared project-command transaction boundary. - Do not add automatic circuit planning, Revit circuit adoption, multi-source management, partial imports, PostgreSQL, background jobs or new permissions. The real sample `docs/spec/kbp-Stromkreis.xlsx` contains project data. It is gitignored and must never be embedded in tests or committed. Use synthetic workbooks for automated tests and accept the real file only as an explicit local path in the reference checker. ## Cable Sizing Module `src/cable-sizing` is an agreed, separate calculator module attached to the circuit list (see `docs/cable-sizing-module.md`). Rules: - The domain code stays pure: no imports from `db/`, `server/` or `frontend/`. - It never writes project data. Results are applied only by an explicit "Übernehmen", which runs the regular `circuit.update` command. - It is opened from the circuit row action button "Leitungsberechnung", never by clicking a grid cell; cable cells keep the normal spreadsheet behavior. - It has no database table and no migration; `/api/cable-sizing/*` is stateless. - Current-carrying capacities and correction factors use DIN VDE 0298-4:2023-06 with Berichtigung 1:2023-10; the exact scope and the separate basis of voltage-drop and protection checks are documented in `docs/cable-sizing-module.md`. Inputs outside the supported tables return no result instead of assuming a factor of 1. Direct burial, grouped method E, aluminium below 25 mm² and neutral-current sizing above 33 % third harmonic remain unsupported until their required inputs or normative basis exist. - Inputs are prefilled from the circuit (power, phase, voltage, weighted cos φ, length, protective device) and remain editable; manual cable entry without a calculation stays possible. ## Future Sizing Do not implement full automatic cable/protection sizing of all circuits unless explicitly requested. The cable sizing module above only suggests values for one circuit on demand. Keep the structure ready for it. Future sizing will need: - circuit total power - phase type - voltage - cosPhi - cable length - cable type - cable cross-section - protection rated current - control requirement such as DALI Users must be able to override sizing suggestions. ## Estimate Balance Module (Grobe Leistungsbilanz) `src/estimate-balance` plus `src/shared/estimate-balance` is a separate module for the early-phase power balance by DIN 276 cost group (see `docs/estimate-balance.md`). Rules: - It has its own tables (`estimate_*`) and never writes circuit, board or project-device data; it only reads device rows for the comparison. - Writes use the project command mechanism (`estimate-balance.insert/.update/ .delete` with complete documents), so revision checks and Undo/Redo apply. - The calculation and comparison live in the import-free `src/shared/estimate-balance/estimate-balance.ts` and are shared by API and UI; displayed values use `roundEstimateValue`. - It is deliberately not part of named/automatic project snapshots or snapshot restore; those operations leave estimate balances unchanged. Portable project transfer format version 2 includes every estimate balance and remaps balance, group and row ids when duplicating a project. Version 1 transfers remain importable and contain no estimate balances. - UI: `/projects/:id/grobbilanz` (Stände) and `/grobbilanz/:balanceId` (editor, `src/frontend/estimate-balance/`). The editor autosaves the whole document debounced (one Undo step per save), keeps invalid raw input local, and on a 409 reloads only if the balance itself changed; otherwise it re-saves with the new revision. `request()` throws `ApiRequestError` (status, code). - A single estimate row can be copied explicitly through the existing ProjectDevice dialog. The dialog is only a prefilled proposal; creation happens after confirmation through `project-device.insert`, with the current project revision and normal Undo/Redo. The copy stores no link back to the estimate row, never places a circuit row automatically, and does not change estimate, snapshot or transfer schemas. Area rows require manual quantity and power because W/m² remains outside the ProjectDevice model. ## Persistence and Migration Rules - SQLite is the currently supported database. - Migration `0000` is the clean release baseline for empty databases. - Pre-baseline development databases, snapshots and stored commands are not supported. - After the first release, never edit an already applied migration. - Back up an existing database before applying a new migration. - Inspect generated SQL; it must contain only the intended schema change. - Keep database backups separate from logical project snapshots. - Do not import the global SQLite singleton into domain services. - Keep synchronous SQLite transaction behavior inside persistence adapters. - Preserve stable UUIDs and explicit transaction boundaries for a later PostgreSQL adapter. ## Current Deferred Work - full electrical sizing - multi-user/PostgreSQL operation - supported production deployment - second/replacement board feeds, including changeover semantics for SV/SiBe - Revit prefill proposals for logical board feeds through the Phase 14 preview and explicit-confirmation workflow - configurable role/rights matrix instead of the fixed `admin`/`user` roles - permanent account lockout (only temporary per-username login throttling exists) - email/one-time-link password provisioning, OIDC/Entra login - linking user accounts to a personnel/employee master table Do not implement these while working on an unrelated phase. ## Documentation and Verification - `README.md` is the setup entry point. - `docs/README.md` is the documentation map. - `docs/current-architecture.md` describes current code paths. - `docs/spec/` contains requirements and roadmap, not proof of implementation. - `docs/archive/` is historical and must not drive current implementation. For a normal code change run the relevant focused tests plus: - `npm test` - `npm run build:api` - `npm run build:web` - `npm run typecheck:scripts` - `npx tsc --noEmit -p tsconfig.next.json` Use a concise imperative commit message for each completed, verified work package. ## Response Style for Codex Be concise. Do not restate the whole specification. Reference the relevant file and section. When uncertain, choose the simplest implementation that preserves the domain model. Do not add libraries unless there is a clear reason. Do not introduce global state management unless needed.