leistungsbilanz-ts/docs/circuit-list-editor-api.md

26 KiB
Raw Permalink Blame History

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 10 enthält Linear-XLSX-Zustand und die in Schema 9 eingeführten 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:

{
  "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:

{
  "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 / Linear XLSX

  • GET|PUT /projects/:projectId/external-model/configuration reads or writes the complete XLSX configuration. PUT requires expectedRevision.
  • POST /projects/:projectId/external-model/configuration/detect detects the sheet layout and columns from an uploaded .xlsx sample without persisting.
  • GET /projects/:projectId/external-model/import-status returns the latest batch metadata, object count and current missing-object count.
  • POST .../initial-import/plan|apply provides the stateless initial plan and its confirmed atomic application. Apply retransmits and reparses the XLSX.
  • POST .../follow-up-import/plan|apply classifies exact Worksharing-ID additions, source changes, unchanged and missing objects. Apply requires the preview hash and source decisions and updates linked row quantities atomically.
  • GET|PUT .../external-model/objects loads one compact project list and applies revision-safe multi-object planning changes.
  • POST .../objects/assign-row|assign-new-row|assign-new-circuit|detach-row changes external-object row assignments through persistent commands.
  • POST .../objects/delete-missing requires confirmed: true, deletes only objects marked missing and adjusts linked row quantities in the same command.
  • POST .../external-model/return-workbook creates the structure-preserving return XLSX for all, deviating or selected objects.
  • GET .../external-model/export-batches returns append-only export metadata.

Uploads accept only .xlsx up to 10 MiB. Worksharing-IDs are exact and case-sensitive; IfcGUID is optional and non-unique. Undo/Redo uses persisted command states and never reparses workbook bytes.

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" } }