forked from jappel/leistungsbilanz-ts
755 lines
37 KiB
Markdown
755 lines
37 KiB
Markdown
# 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`.
|
||
R5 adds stateless XLSX preview/planning and the six-step initial-import wizard.
|
||
`external-model.apply-initial-import` stores the configuration and complete
|
||
external model state atomically; its inverse powers persistent Undo/Redo
|
||
without parsing workbook bytes. Apply retransmits and reparses the XLSX,
|
||
verifies its preview SHA-256 and creates no circuit, equipment identifier or
|
||
CircuitDeviceRow.
|
||
R6 adds one compact project-wide external-object GET without pagination or
|
||
batch payloads. The Revit page filters and sorts it in the client and renders a
|
||
virtualized window. `external-object.update-planning` applies multi-object
|
||
board, ProjectDevice and planning-value changes as one persistent command;
|
||
linked objects are excluded because they follow their CircuitDeviceRow board.
|
||
R9 adds the complete Linear-XLSX follow-up import. It matches Worksharing-IDs
|
||
exactly, separates source decisions from return deviations, updates accepted
|
||
linked quantities atomically, retains missing objects and compacts every older
|
||
batch to metadata. `external-import.apply-follow-up` stores complete inverse
|
||
states, so Undo/Redo never parses Excel. A separately confirmed
|
||
`external-model-object.delete-missing` command removes missing objects and
|
||
adjusts linked row quantities; Undo restores objects, links and quantities.
|
||
|
||
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
|
||
`<thead>` 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 through R9 are complete and R10 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.
|