forked from jappel/leistungsbilanz-ts
545 lines
26 KiB
Markdown
545 lines
26 KiB
Markdown
# Circuit List Editor API
|
||
|
||
## Scope
|
||
|
||
The circuit-first editor uses tree, circuit, row and project-device endpoints.
|
||
All paths below are mounted below `/api`. There is no Consumer application API.
|
||
|
||
Project responses from `GET /projects`, `GET /projects/:projectId` and the
|
||
circuit-tree endpoint include `currentRevision`. Versioned project commands use
|
||
this value for optimistic concurrency checks.
|
||
|
||
### Project Commands and History
|
||
|
||
- `GET /projects/:projectId/history`
|
||
- returns `currentRevision`, `undoDepth`, `redoDepth` and the current top
|
||
change-set id for both persistent stacks
|
||
- `GET /projects/:projectId/history/revisions`
|
||
- returns revision metadata in descending `revisionNumber` order
|
||
- optional query: `limit` (default `25`, maximum `100`) and exclusive
|
||
`beforeRevision`
|
||
- returns `projectId`, `currentRevision`, `revisions` and
|
||
`nextBeforeRevision`; use the latter as the next page's
|
||
`beforeRevision`
|
||
- each revision contains ids, number, timestamp, actor, source, description,
|
||
command type and payload schema version
|
||
- stored forward/inverse command payloads are intentionally not exposed
|
||
- `POST /projects/:projectId/commands`
|
||
- executes a supported versioned command as a new user revision
|
||
- body: `{ "expectedRevision": 0, "command": { ... } }`
|
||
- `POST /projects/:projectId/history/undo`
|
||
- `POST /projects/:projectId/history/redo`
|
||
- body: `{ "expectedRevision": 1 }`
|
||
- executes the eligible inverse or forward command as a new auditable
|
||
revision
|
||
- `GET /projects/:projectId/snapshots`
|
||
- lists named and automatic logical snapshot metadata, including `kind`,
|
||
without returning payload JSON
|
||
- `POST /projects/:projectId/snapshots`
|
||
- body:
|
||
`{ "expectedRevision": 12, "name": "Vor Ausschreibung", "description": "Optional" }`
|
||
- atomically captures the complete project-scoped runtime state at the
|
||
expected revision
|
||
- returns `201` with source revision, schema version, SHA-256 and creation
|
||
metadata
|
||
- snapshot creation does not increment the project revision or alter
|
||
undo/redo stacks
|
||
- duplicate names and stale revisions return `409`
|
||
- the central revision boundary additionally creates `automatic` snapshots
|
||
after 25 further revisions and retains the newest 12 per project; named
|
||
snapshots are never removed by this policy
|
||
- `POST /projects/:projectId/snapshots/:snapshotId/restore`
|
||
- body: `{ "expectedRevision": 13 }`
|
||
- verifies the stored payload checksum and atomically restores the complete
|
||
supported project state as revision `14` with source `restore`
|
||
- stores the complete pre-restore state as the inverse command, so project
|
||
Undo/Redo can revert or repeat the restore after a restart
|
||
- stale revisions or state checks return `409`; unknown snapshots return
|
||
`404`
|
||
- clients cannot submit arbitrary `project.restore-state` payloads through
|
||
the generic command endpoint
|
||
|
||
Grobbilanzen sind bewusst kein Teil dieser logischen Snapshots. Der portable
|
||
Projektexport unter `GET /projects/:projectId/export` verwendet dagegen
|
||
Transferformat 2 und enthält alle Grobbilanzen. Die beiden Importwege
|
||
`POST /projects/:projectId/import` und `POST /projects/import` prüfen ihre
|
||
Prüfsumme; Ersetzen übernimmt die Grobbilanzen atomar und mit Undo/Redo,
|
||
Duplizieren vergibt neue IDs. Transferformat 1 bleibt ohne Grobbilanzen
|
||
importierbar.
|
||
|
||
Das aktuelle Projekt-Snapshot-Schema 9 enthält die logischen Einspeisungsverweise
|
||
der Verteilungen; Versionen 1–8 werden mit leeren Verweisen hochgestuft. Schema 8
|
||
enthält außerdem die optionale Netto-Raumfläche `rooms[].areaM2`.
|
||
Snapshot-Versionen 1–7 werden dafür mit
|
||
`areaM2 = null` hochgestuft. Restore, Export und Import erhalten vorhandene
|
||
Flächen und Einspeisungsverweise; beim Duplizieren werden Board- und Circuit-IDs
|
||
neu zugeordnet. Eine eigene Transferformat-Version ist nicht nötig, weil Format 2 den
|
||
versionierten Projektsnapshot einbettet.
|
||
|
||
The public dispatcher supports the Circuit and CircuitDeviceRow commands
|
||
`circuit.update`, `circuit.insert`, `circuit.delete`,
|
||
`circuit-device-row.update`, `circuit-device-row.insert`,
|
||
`circuit-device-row.delete`, `circuit-device-row.move`,
|
||
`circuit-device-row.move-with-new-circuit`, `circuit.reorder-section`,
|
||
`circuit.reorder-sections` and `circuit.renumber-section`.
|
||
|
||
Protected-board structure uses
|
||
`distribution-board-component.insert|update|delete`,
|
||
`circuit-group.insert|update|delete|reorder|renumber`,
|
||
`circuit.move-group`, `circuit-group.delete-subtree` and
|
||
`circuit-protection.update`. A subtree restore is normally emitted only as the
|
||
persisted inverse of a delete. The dispatcher also supports the documented
|
||
project-device, project settings, distribution-board and project-location
|
||
commands. The generic editor commands listed here use schema version `1`.
|
||
`project.update-settings` uses version `3` (including the print cover fields)
|
||
and still accepts version `2` and supported baseline version `1` commands
|
||
without changing the newer cover fields or building classification.
|
||
Pre-baseline formats remain unsupported. Unsupported types or schema versions
|
||
are rejected. Insert commands contain the complete entity or circuit block
|
||
with stable ids; delete commands include the expected parent identity. Circuit
|
||
snapshots contain zero, one or multiple complete device rows. Move commands
|
||
contain each row's expected and target circuit plus its exact expected and
|
||
target sort order. This makes deletion and moves undoable without generating
|
||
replacement identities or renumbering circuits. Existing Circuit and
|
||
CircuitDeviceRow cell edits, standalone insert/delete actions, device-row
|
||
moves, reorders and explicit renumbering in the editor consume these endpoints.
|
||
|
||
`circuit-device-row.move` targets existing circuits in the same circuit list.
|
||
`circuit-device-row.move-with-new-circuit` atomically creates exactly one
|
||
explicitly identified empty target circuit and moves one or multiple existing
|
||
rows into it. Its inverse restores every row to its exact prior position and
|
||
deletes the generated circuit only if its fields and complete row set still
|
||
match the recorded state.
|
||
|
||
`circuit.reorder-section` requires one assignment for every circuit currently
|
||
in the section. Each assignment records the expected and target `sortOrder`.
|
||
The command and its inverse change no circuit field other than `sortOrder`;
|
||
equipment identifiers and complete device-row blocks remain unchanged.
|
||
|
||
`circuit.reorder-sections` contains one complete assignment block per affected
|
||
section. All blocks are validated before any write and commit or roll back
|
||
together as one revision and one undo step. The editor uses it when applying a
|
||
sorted view that changes multiple sections.
|
||
|
||
`circuit.renumber-section` is an explicit operation requiring one assignment
|
||
for every circuit in the section. Assignments contain expected and target
|
||
equipment identifiers. The store rejects stale values, duplicate targets and
|
||
targets occupied by other sections, then applies swaps through collision-safe
|
||
temporary identifiers. Undo restores the exact prior identifiers; sort
|
||
positions and device rows remain unchanged.
|
||
|
||
`circuit-group.reorder` changes only complete group sort assignments.
|
||
`circuit-group.renumber` carries every affected group, child circuit and group
|
||
component with expected and target identifiers so swaps can be applied
|
||
collision-safely. `circuit.move-group` changes one complete circuit's group,
|
||
BMK and position and stores the deterministic target BMK. The target group must
|
||
have the same category.
|
||
|
||
`circuit-group.delete-subtree` carries the complete warned group state,
|
||
including both protection layers and every device-row link/override value. The
|
||
server captures the current subtree again inside the transaction and rejects a
|
||
stale or incomplete snapshot. Undo restores the same UUIDs through the stored
|
||
inverse command.
|
||
|
||
`circuit-protection.update` inserts or replaces the complete validated
|
||
one-to-one circuit protection state. Users cannot remove the required
|
||
protection row; removal is available only to a stored inverse when undoing its
|
||
initial creation.
|
||
|
||
`project-device.sync-rows` represents `synchronize`, `disconnect` and
|
||
`reconnect` operations. Every selected row carries complete expected and target
|
||
snapshots of all ProjectDevice-sync fields, the link and `overriddenFields`.
|
||
All rows, the inverse command, revision and history-stack transition commit or
|
||
roll back together. Disconnect/reconnect may only change the link; stale row
|
||
snapshots and cross-project devices or rows are rejected.
|
||
|
||
`project-device.update` changes one or multiple canonical ProjectDevice fields
|
||
and derives its inverse from the persisted device. It validates project
|
||
ownership and never writes linked `CircuitDeviceRow` values; synchronization
|
||
remains a separate explicit command.
|
||
|
||
`project-device.insert` stores the complete canonical device with a stable id.
|
||
User inserts cannot attach existing rows. `project-device.delete` captures the
|
||
complete device and every currently linked row before deletion. Its inverse
|
||
recreates the same device and reconnects only rows whose complete disconnected
|
||
snapshot still matches; device, links, revision and history transition are
|
||
atomic.
|
||
|
||
### Project Device CRUD
|
||
|
||
- `GET /project-devices/projects/:projectId`
|
||
- lists the project's reusable devices
|
||
- `POST /project-devices/projects/:projectId`
|
||
- `PUT /project-devices/projects/:projectId/:projectDeviceId`
|
||
- request: all canonical device fields plus `expectedRevision`
|
||
- response: `{ "projectDevice": { ... }, "revision": { ... }, "history": { ... } }`
|
||
- `DELETE /project-devices/projects/:projectId/:projectDeviceId`
|
||
- request: `{ "expectedRevision": 12 }`
|
||
- response: the project command result with the updated history state
|
||
- `POST /project-devices/projects/:projectId/import-global/:globalDeviceId`
|
||
- request: `{ "expectedRevision": 12 }`
|
||
- creates an independent project device with a stable UUID
|
||
- response shape matches ProjectDevice create/update
|
||
|
||
These write endpoints execute the typed `project-device.update`,
|
||
`project-device.insert` and `project-device.delete` commands. A stale revision
|
||
returns `409 PROJECT_REVISION_CONFLICT`. Updating a project device still never
|
||
synchronizes linked circuit rows implicitly.
|
||
|
||
### Project Settings
|
||
|
||
- `PUT /projects/:projectId`
|
||
- request:
|
||
`{ "expectedRevision": 12, "isPublicBuilding": true, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400, "enabledDistributionBoardSupplyTypes": ["AV", "MSR", "SiBe"], ...projectMetadata }`
|
||
- optional print cover fields `siteAddress` (max. 500), `workStage` (max.
|
||
100), `projectLead` and `projectEditor` (max. 200 each); omitted fields keep
|
||
their value, empty strings are stored as `null`
|
||
- executes `project.update-settings` as one atomic revision
|
||
- response:
|
||
`{ "project": { ... }, "revision": { ... }, "history": { ... } }`
|
||
- persistent Undo/Redo restores metadata, the public-building flag, voltage
|
||
values and enabled distribution-board supply types together; project-device and circuit
|
||
voltages are recalculated from the restored project settings in the same
|
||
transaction
|
||
- project-device and circuit voltage are derived values and are not accepted
|
||
as editable frontend fields
|
||
- at least one of `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe` must be enabled;
|
||
a supply type currently used by a distribution board cannot be disabled
|
||
- unchanged values are rejected without creating a revision; a stale
|
||
revision returns `409 PROJECT_REVISION_CONFLICT`
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"expectedRevision": 0,
|
||
"description": "Rename circuit",
|
||
"command": {
|
||
"schemaVersion": 1,
|
||
"type": "circuit.update",
|
||
"payload": {
|
||
"circuitId": "cir_1",
|
||
"changes": [
|
||
{ "field": "displayName", "value": "Sockets East" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
A stale `expectedRevision` returns HTTP `409` with
|
||
`PROJECT_REVISION_CONFLICT`. Undo or redo without an eligible stack entry
|
||
returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
|
||
|
||
## Estimate Balance (Grobe Leistungsbilanz)
|
||
|
||
Separate module under `/projects/:projectId/estimate-balances`; complete
|
||
documents are written through `estimate-balance.insert/.update/.delete` project
|
||
commands. Endpoints, payloads and calculation rules: `docs/estimate-balance.md`.
|
||
|
||
## Circuit-First Endpoints
|
||
|
||
### Distribution Board Setup
|
||
|
||
- `POST /projects/:projectId/distribution-boards`
|
||
- body:
|
||
`{ "name": "UV-02", "floorId": "floor_1", "supplyType": "SV", "expectedRevision": 12 }`
|
||
- `floorId` may be `null`; a non-null floor must belong to the project
|
||
- `supplyType` is one of `AV`, `SV`, `EV`, `USV`, `MSR` or `SiBe` and must
|
||
be enabled in the project settings
|
||
- executes `distribution-board.insert` with stable ids for the distribution
|
||
board, its circuit list, the three default circuit groups, main switch
|
||
`-Q0` and surge protective device `-FA`
|
||
- response:
|
||
`{ "distributionBoard": { ... }, "revision": { ... }, "history": { ... } }`
|
||
- persistent Undo removes only the unchanged and still-empty generated
|
||
structure; Redo restores the same ids
|
||
- stale revisions return `409 PROJECT_REVISION_CONFLICT`
|
||
- `PUT /projects/:projectId/distribution-boards/:distributionBoardId`
|
||
- body:
|
||
`{ "floorId": null, "supplyType": "AV", "simultaneityFactor": 1, "feedingDistributionBoardId": "board_hv", "feedingCircuitId": "circuit_out", "expectedRevision": 13 }`
|
||
- optional info-card fields: `boardNumber` (max. 50), `location`,
|
||
`fedFrom` (max. 200 each) and `remark` (max. 2000); strings are trimmed
|
||
and an empty string is stored as `null`
|
||
- `feedingDistributionBoardId` and `feedingCircuitId` are optional nullable
|
||
fields. A feeding circuit requires a feeding board and must belong to it;
|
||
self-links and cycles are rejected. A board-only link creates hierarchy
|
||
without assigning its load to an upstream circuit.
|
||
- executes `distribution-board.update` schema version 2; all submitted values, including the
|
||
submitted info fields, are restored together by persistent Undo/Redo
|
||
- `fedFrom` remains free text for external feeds such as HAK, metering,
|
||
transformer or generator and for supplementary notes
|
||
- The project-command endpoint additionally accepts
|
||
`distribution-board.create-feeder-circuit` with an empty complete circuit
|
||
snapshot and an embedded version-2 `distribution-board.update`. Both writes
|
||
are one transaction, revision and Undo step. The generated circuit has no
|
||
device row; Redo keeps its UUID and BMK. The history-only inverse
|
||
`distribution-board.remove-feeder-circuit` restores the previous board values
|
||
and removes only the unchanged circuit.
|
||
- `POST /projects/:projectId/distribution-boards/:distributionBoardId/copy`
|
||
- body: `{ "name": "UV-02 Kopie", "expectedRevision": 14 }`
|
||
- duplicates the complete distribution-board subtree with new owning UUIDs
|
||
while preserving project-device, room and incoming feeding links
|
||
- response includes the new `distributionBoard`, its `circuitList`, the
|
||
revision and current history state
|
||
- `DELETE /projects/:projectId/distribution-boards/:distributionBoardId`
|
||
- body: `{ "expectedRevision": 15 }`
|
||
- removes the complete, exactly captured distribution-board subtree in one
|
||
transaction
|
||
- rejects deletion while another board references this board or one of its
|
||
circuits as a feeder; circuit and populated-group deletion use the same guard
|
||
- response includes `distributionBoardId`, the revision and current history
|
||
state
|
||
- Copy and deletion use `distribution-board.insert-subtree` and
|
||
`distribution-board.delete-subtree`. Persistent Undo/Redo restores or
|
||
removes the same complete subtree, including protection devices.
|
||
|
||
### Project Buildings, Floors and Rooms
|
||
|
||
- `GET /projects/:projectId/buildings`
|
||
- `POST /projects/:projectId/buildings`
|
||
- body: `{ "name": "Haus A", "expectedRevision": 12 }`
|
||
- executes `project-building.insert` with a stable building id
|
||
- response: `{ "building": { ... }, "revision": { ... }, "history": { ... } }`
|
||
- `PUT /projects/:projectId/buildings/:buildingId`
|
||
- body: `{ "name": "Haus B", "expectedRevision": 13 }` (name only)
|
||
- `DELETE /projects/:projectId/buildings/:buildingId`
|
||
- body: `{ "expectedRevision": 14 }`
|
||
- rejects buildings with assigned floors
|
||
- `POST /projects/:projectId/floors`
|
||
- body: `{ "name": "EG", "buildingId": "building_1", "expectedRevision": 13 }`
|
||
(`buildingId` optional, `null` = no building)
|
||
- executes `project-floor.insert` (command version 2) with a stable floor id
|
||
- response: `{ "floor": { ... }, "revision": { ... }, "history": { ... } }`
|
||
- `PUT /projects/:projectId/floors/:floorId`
|
||
- body: `{ "name": "1. OG", "buildingId": null, "expectedRevision": 14 }`
|
||
- executes an exact persistent floor update of name and/or building; a
|
||
missing `buildingId` keeps the current building
|
||
- `DELETE /projects/:projectId/floors/:floorId`
|
||
- body: `{ "expectedRevision": 15 }`
|
||
- rejects floors with assigned rooms or distribution boards
|
||
- `POST /projects/:projectId/rooms`
|
||
- body:
|
||
`{ "floorId": "floor_1", "roomNumber": "001", "roomName": "Technik", "areaM2": 18.75, "expectedRevision": 14 }`
|
||
- executes `project-room.insert` command version 2 with a stable room id;
|
||
`floorId` and `areaM2` are optional. An omitted or explicit `null` area
|
||
creates a room without area; a value must be finite and non-negative.
|
||
- response: `{ "room": { ... }, "revision": { ... }, "history": { ... } }`
|
||
- `PUT /projects/:projectId/rooms/:roomId`
|
||
- body:
|
||
`{ "floorId": null, "roomNumber": "101", "roomName": "Büro", "areaM2": null, "expectedRevision": 16 }`
|
||
- updates number, name, optional floor and area as one persistent version-2
|
||
command. If `areaM2` is omitted, the current area is retained; `null`
|
||
explicitly clears it.
|
||
- `DELETE /projects/:projectId/rooms/:roomId`
|
||
- body: `{ "expectedRevision": 17 }`
|
||
- rejects rooms referenced by device rows
|
||
- Persistent Undo/Redo restores exact previous and target records without
|
||
silently clearing foreign keys.
|
||
- Stored version-1 room commands remain executable and are upgraded with
|
||
`areaM2 = null`. Exact expected-record checks reject an old update or delete
|
||
if a newer command has added an area, so legacy history cannot silently
|
||
erase it.
|
||
- Stale revisions return `409 PROJECT_REVISION_CONFLICT`.
|
||
|
||
### Tree Endpoint
|
||
|
||
- `GET /projects/:projectId/circuit-trees`
|
||
- returns all circuit trees for one project from one project-wide read and is
|
||
used by the print view and feeding-board selection
|
||
- `GET /projects/:projectId/circuit-lists/:circuitListId/tree`
|
||
- Purpose: returns fixed header components, grouped sections with optional
|
||
protection components, complete circuit/device-row blocks and auxiliary
|
||
footer components. Circuit and component protection devices use their
|
||
separate one-to-one DTOs. Calculated row, circuit, section and board totals
|
||
remain included. Circuit totals contain the demand of linked downstream
|
||
boards. `downstreamBoardLoads` exposes those contributions as read-only
|
||
values; the grid combines exactly one downstream board without device rows
|
||
into the circuit row and keeps child rows for mixed or multiple loads.
|
||
`feedingReference` resolves the current board/circuit labels by id.
|
||
|
||
Response sketch:
|
||
|
||
```json
|
||
{
|
||
"circuitListId": "cl_1",
|
||
"currentRevision": 12,
|
||
"headerComponents": [
|
||
{
|
||
"id": "cmp_main",
|
||
"equipmentIdentifier": "-Q0",
|
||
"name": "Hauptschalter",
|
||
"role": "main_switch",
|
||
"placement": "header"
|
||
}
|
||
],
|
||
"sections": [
|
||
{
|
||
"id": "sec_1",
|
||
"key": "lighting",
|
||
"prefix": "-1F1.",
|
||
"category": "lighting",
|
||
"groupNumber": 1,
|
||
"components": [
|
||
{
|
||
"id": "cmp_rcd",
|
||
"equipmentIdentifier": "-1Q1.0",
|
||
"name": "Gruppen-FI",
|
||
"role": "group_residual_current_protection",
|
||
"placement": "group",
|
||
"protectionDevice": {
|
||
"type": "FI",
|
||
"ratedCurrentA": 40,
|
||
"rcdType": "A",
|
||
"ratedResidualCurrentMa": 30
|
||
}
|
||
}
|
||
],
|
||
"circuits": [
|
||
{
|
||
"id": "cir_1",
|
||
"equipmentIdentifier": "-1F1.1",
|
||
"circuitTotalPower": 4.2,
|
||
"protectionDevice": {
|
||
"type": "LS",
|
||
"ratedCurrentA": 10,
|
||
"tripCharacteristic": "B"
|
||
},
|
||
"deviceRows": [
|
||
{
|
||
"id": "row_1",
|
||
"displayName": "Office lights",
|
||
"rowTotalPower": 1.8
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"footerComponents": [
|
||
{
|
||
"id": "cmp_actor",
|
||
"equipmentIdentifier": "-K1",
|
||
"name": "KNX Schaltaktor",
|
||
"role": "auxiliary",
|
||
"placement": "footer"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Circuit Structure
|
||
|
||
- `GET /projects/:projectId/circuit-sections/:sectionId/next-identifier`
|
||
- preview next identifier for section (`prefix + maxSuffix + 1`)
|
||
- returns 404 if the section does not belong to the given project
|
||
|
||
Circuit and device-row field updates, standalone insertions/deletions, single
|
||
or bulk device-row moves, circuit reorders and explicit renumbering are
|
||
available only as the corresponding versioned commands through
|
||
`POST /projects/:projectId/commands`. There are no direct field-update PATCH,
|
||
structure POST, move, reorder, renumber, identifier-restore, Circuit DELETE or
|
||
CircuitDeviceRow DELETE routes.
|
||
|
||
|
||
## Linked Project Device Review
|
||
|
||
- `GET /project-devices/projects/:projectId/:projectDeviceId/links`
|
||
- returns all linked circuit device rows with distribution-board/circuit context and field differences
|
||
- `POST /project-devices/projects/:projectId/:projectDeviceId/synchronize`
|
||
- applies only explicitly selected fields to explicitly selected linked rows
|
||
- request: selected `rowIds`, selected `fields` and `expectedRevision`
|
||
- response: updated preview, revision and project history state
|
||
- `POST /project-devices/projects/:projectId/:projectDeviceId/disconnect`
|
||
- disconnects explicitly selected rows without changing their local values
|
||
- request: selected `rowIds` and `expectedRevision`
|
||
- response: updated preview, revision and project history state
|
||
|
||
Both writes execute `project-device.sync-rows`. Undo uses the project-wide
|
||
`POST /projects/:projectId/history/undo` endpoint; reconnect is the validated
|
||
inverse command. The former direct restore/reconnect endpoints are removed.
|
||
|
||
`displayName` is included in the comparison but is not selected by default in the UI. Updating a
|
||
project device never triggers synchronization implicitly.
|
||
|
||
## Cable Sizing (optional module)
|
||
|
||
Stateless calculator endpoints mounted at `/api/cable-sizing`. They neither
|
||
read nor write project data; applying a result uses `circuit.update` through
|
||
`POST /projects/:projectId/commands`.
|
||
|
||
- `POST /cable-sizing/calculate`
|
||
- request: phase, mode (`power` | `current`), powerKw or currentA, cosPhi,
|
||
voltage, lengthM, layingMethod (`A1` | `A2` | `B1` | `B2` | `C` | `D` |
|
||
`E`), conductorMaterial, insulation (`pvc` | `xlpe`),
|
||
ambientTemperatureC, groupingCircuits, maxVoltageDropPercent,
|
||
harmonicNeutralLoad, optional `existingProtection`
|
||
(`{ ratedCurrentA, kind: "breaker" | "fuse" }`) and circuitCategory
|
||
- returns `{ "result": { ... }, "alerts": [ ... ] }`; `result.supported`
|
||
is `false` with `unsupportedReason` when temperature or grouping lie
|
||
outside the tables or the neutral current needed above 33 % third
|
||
harmonic is unavailable
|
||
- unknown fields are rejected (`400`)
|
||
- `GET /cable-sizing/laying-methods`
|
||
- `GET /cable-sizing/insulation-materials`
|
||
|
||
Full request example and calculation rules: `docs/cable-sizing-module.md`.
|
||
|
||
## Revit CSV Foundation
|
||
|
||
- `GET /projects/:projectId/external-csv/configuration`
|
||
- returns the complete versioned project configuration or `null`
|
||
- `PUT /projects/:projectId/external-csv/configuration`
|
||
- request: `expectedRevision` and the complete validated configuration
|
||
- executes `external-csv-configuration.update`
|
||
- returns configuration, revision and project history state
|
||
- `POST /projects/:projectId/external-csv/preview`
|
||
- request: file name and Base64-encoded CSV content up to 18 MB
|
||
- requires a stored project configuration
|
||
- returns transport metadata, SHA-256, row classifications, suspect row
|
||
numbers and mapped source values for every recognized IFC object
|
||
- does not persist a draft, mutate project state or create a revision
|
||
- `POST /projects/:projectId/external-csv/initial-import/plan`
|
||
- reparses the file and returns grouped room/family decisions plus current
|
||
room, distribution-board and ProjectDevice catalogs
|
||
- is stateless and rejects an already imported external source
|
||
- `POST /projects/:projectId/external-csv/initial-import/apply`
|
||
- requires the expected revision, configuration version, file hash and one
|
||
decision for every room and exact family/type group
|
||
- reparses the file, blocks unknown family/type values and executes
|
||
`external-import.apply-initial` as one undoable project revision
|
||
- creates no Circuit or CircuitDeviceRow
|
||
- `GET /projects/:projectId/circuit-lists/:circuitListId/external-objects`
|
||
- resolves the circuit list's distribution board server-side
|
||
- returns only external objects assigned to that board, with unassigned
|
||
objects first, their source room/marker values and current row-link status
|
||
- returns an empty list when the optional Revit import does not exist
|
||
- is read-only and exposes no stored command payloads or import bytes
|
||
|
||
Parser errors expose stable codes such as `header-not-found`,
|
||
`ambiguous-header`, `invalid-ifc-guid` and `duplicate-ifc-guid`. The confirmed
|
||
import uploads and parses the file again and verifies its hash.
|
||
|
||
## Document Branding (print view)
|
||
|
||
Office-wide branding for the print view. These endpoints are mounted at
|
||
`/api/document-branding`, outside `/api/projects`, because the data is global
|
||
and intentionally not part of project revisions, snapshots or transfer.
|
||
|
||
- `GET /document-branding`
|
||
- returns `{ "logo": { "mimeType", "fileName", "sizeBytes", "updatedAtIso" } | null, "office": { "officeName", "officeAddress" } }`
|
||
- `GET /document-branding/logo`
|
||
- returns the stored image bytes with its media type, or `404` without logo
|
||
- responses carry `X-Content-Type-Options: nosniff` and a restrictive
|
||
Content-Security-Policy so an SVG logo cannot execute scripts
|
||
- `PUT /document-branding/logo`
|
||
- request: `{ "fileName": "logo.png", "mimeType": "image/png", "contentBase64": "..." }`
|
||
- accepted types: `image/png`, `image/jpeg`, `image/svg+xml`; at most 2 MB
|
||
after decoding; the content signature must match the declared type
|
||
- replaces an existing logo; returns the logo metadata
|
||
- `DELETE /document-branding/logo`
|
||
- removes the logo; returns `204`
|
||
- `PUT /document-branding/office`
|
||
- request: `{ "officeName": "…" | null, "officeAddress": "…" | null }`
|
||
(at most 200 / 500 characters; empty text is stored as not set)
|
||
- returns `{ "office": { "officeName", "officeAddress" } }`
|