17 KiB
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,redoDepthand the current top change-set id for both persistent stacks
- returns
GET /projects/:projectId/history/revisions- returns revision metadata in descending
revisionNumberorder - optional query:
limit(default25, maximum100) and exclusivebeforeRevision - returns
projectId,currentRevision,revisionsandnextBeforeRevision; use the latter as the next page'sbeforeRevision - each revision contains ids, number, timestamp, actor, source, description, command type and payload schema version
- stored forward/inverse command payloads are intentionally not exposed
- returns revision metadata in descending
POST /projects/:projectId/commands- executes a supported versioned command as a new user revision
- body:
{ "expectedRevision": 0, "command": { ... } }
POST /projects/:projectId/history/undoPOST /projects/:projectId/history/redo- body:
{ "expectedRevision": 1 } - executes the eligible inverse or forward command as a new auditable revision
- body:
GET /projects/:projectId/snapshots- lists named and automatic logical snapshot metadata, including
kind, without returning payload JSON
- lists named and automatic logical snapshot metadata, including
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
201with 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
automaticsnapshots after 25 further revisions and retains the newest 12 per project; named snapshots are never removed by this policy
- body:
POST /projects/:projectId/snapshots/:snapshotId/restore- body:
{ "expectedRevision": 13 } - verifies the stored payload checksum and atomically restores the complete
supported project state as revision
14with sourcerestore - 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 return404 - clients cannot submit arbitrary
project.restore-statepayloads through the generic command endpoint
- body:
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 2 and still accepts supported baseline
version 1 commands without changing the newer 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/:projectIdPUT /project-devices/projects/:projectId/:projectDeviceId- request: all canonical device fields plus
expectedRevision - response:
{ "projectDevice": { ... }, "revision": { ... }, "history": { ... } }
- request: all canonical device fields plus
DELETE /project-devices/projects/:projectId/:projectDeviceId- request:
{ "expectedRevision": 12 } - response: the project command result with the updated history state
- request:
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
- request:
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 } - executes
project.update-settingsas 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,SiBemust 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
- request:
Example:
{
"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.
Circuit-First Endpoints
Distribution Board Setup
POST /projects/:projectId/distribution-boards- body:
{ "name": "UV-02", "floorId": "floor_1", "supplyType": "SV", "expectedRevision": 12 } floorIdmay benull; a non-null floor must belong to the projectsupplyTypeis one ofAV,SV,EV,USV,MSRorSiBeand must be enabled in the project settings- executes
distribution-board.insertwith stable ids for the distribution board, its circuit list, the three default circuit groups, main switch-Q0and 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
- body:
PUT /projects/:projectId/distribution-boards/:distributionBoardId- body:
{ "floorId": null, "supplyType": "AV", "expectedRevision": 13 } - executes
distribution-board.update; floor and supply type are restored together by persistent Undo/Redo
- body:
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 and room links
- response includes the new
distributionBoard, itscircuitList, the revision and current history state
- body:
DELETE /projects/:projectId/distribution-boards/:distributionBoardId- body:
{ "expectedRevision": 15 } - removes the complete, exactly captured distribution-board subtree in one transaction
- response includes
distributionBoardId, the revision and current history state
- body:
- Copy and deletion use
distribution-board.insert-subtreeanddistribution-board.delete-subtree. Persistent Undo/Redo restores or removes the same complete subtree, including protection devices.
Project Floors and Rooms
POST /projects/:projectId/floors- body:
{ "name": "EG", "expectedRevision": 13 } - executes
project-floor.insertwith a stable floor id - response:
{ "floor": { ... }, "revision": { ... }, "history": { ... } }
- body:
PUT /projects/:projectId/floors/:floorId- body:
{ "name": "1. OG", "expectedRevision": 14 } - executes an exact persistent floor update
- body:
DELETE /projects/:projectId/floors/:floorId- body:
{ "expectedRevision": 15 } - rejects floors with assigned rooms or distribution boards
- body:
POST /projects/:projectId/rooms- body:
{ "floorId": "floor_1", "roomNumber": "001", "roomName": "Technik", "expectedRevision": 14 } - executes
project-room.insertwith a stable room id;floorIdis optional and must belong to the project when present - response:
{ "room": { ... }, "revision": { ... }, "history": { ... } }
- body:
PUT /projects/:projectId/rooms/:roomId- body:
{ "floorId": null, "roomNumber": "101", "roomName": "Büro", "expectedRevision": 16 } - updates number, name and optional floor as one persistent command
- body:
DELETE /projects/:projectId/rooms/:roomId- body:
{ "expectedRevision": 17 } - rejects rooms referenced by device rows
- body:
- Persistent Undo/Redo restores exact previous and target records without silently clearing foreign keys.
- Stale revisions return
409 PROJECT_REVISION_CONFLICT.
Tree Endpoint
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.
Response sketch:
{
"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 /circuit-sections/:sectionId/next-identifier- preview next identifier for section (
prefix + maxSuffix + 1)
- preview next identifier for section (
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, selectedfieldsandexpectedRevision - 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
rowIdsandexpectedRevision - 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.