docs: render architecture & flow diagrams as draw.io charts

Replace ASCII-art diagrams across the README and docs/ with editable
.drawio sources plus exported PNGs, so the diagrams render clearly in
rendered markdown and can be maintained/regenerated instead of being
hand-edited as fragile text art. Non-diagram blocks (code, folder
trees, UI wireframes) were left as text.
This commit is contained in:
Joseph Doherty
2026-05-31 23:32:53 -04:00
parent 3763f6d2d8
commit bdee12f4e9
71 changed files with 2461 additions and 516 deletions
+4 -96
View File
@@ -110,102 +110,10 @@ Both stacks share the infrastructure services in [`infra/`](infra/) (MS SQL, LDA
### Architecture Diagram (Logical) ### Architecture Diagram (Logical)
``` ![Logical architecture](diagrams/architecture-logical.png)
Users (Blazor Server) <!-- source: diagrams/architecture-logical.drawio — edit, then re-export with export-drawio.sh -->
Load Balancer
┌────────────────────────┼────────────────────────────┐
│ CENTRAL CLUSTER │
│ (2-node active/standby) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Template │ │Deployment│ │ Central │ │
│ │ Engine │ │ Manager │ │ UI │ Blazor Svr │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Security │ │ Config │ │ Health │ │
│ │ & Auth │ │ DB │ │ Monitor │ │
│ │ (JWT/LDAP)│ │ (EF+IAud)│ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ │
│ │ Inbound │ ◄── External Systems (X-API-Key) │
│ │ API │ POST /api/{method}, JSON │
│ └──────────┘ │
│ ┌──────────┐ │
│ │ Mgmt │ ◄── CLI (ClusterClient) │
│ │ Service │ ManagementActor + Receptionist │
│ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Ntf │ │ Site │ │ Audit │ Observ. / │
│ │ Outbox │ │ Call │ │ Log │ Audit area │
│ │ (#21) │ │ Audit │ │ (#23) │ │
│ │ │ │ (#22) │ │ │ │
│ └────▲─────┘ └────▲─────┘ └────▲─────┘ │
│ │ ingests │ ingests │ ingests │
│ │ (S&F) │ (telemetry)│ (telemetry + │
│ │ │ │ direct-write │
│ │ │ │ from Ntf Outbox │
│ │ │ │ & Inbound API) │
│ ┌───────────────────────────────────┐ │
│ │ Akka.NET Communication Layer │ │
│ │ ClusterClient: command/control │ │
│ │ gRPC Client: real-time streams │ │
│ │ (correlation IDs, per-pattern │ │
│ │ timeouts, message ordering) │ │
│ └──────────────┬────────────────────┘ │
│ ┌──────────────┴────────────────────┐ │
│ │ Configuration Database (EF) │──► MS SQL │
│ └───────────────────────────────────┘ (Config DB)│
│ │ Machine Data DB│
└─────────────────┼───────────────────────────────────┘
│ Akka.NET Remoting (command/control)
│ gRPC HTTP/2 (real-time data, port 8083)
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ SITE A │ │ SITE B │ │ SITE N │
│ (2-node)│ │ (2-node)│ │ (2-node)│
│ ┌─────┐ │ │ ┌─────┐ │ │ ┌─────┐ │
│ │Data │ │ │ │Data │ │ │ │Data │ │
│ │Conn │ │ │ │Conn │ │ │ │Conn │ │
│ │Layer │ │ │ │Layer │ │ │ │Layer │ │
│ ├─────┤ │ │ ├─────┤ │ │ ├─────┤ │
│ │Site │ │ │ │Site │ │ │ │Site │ │
│ │Runtm│ │ │ │Runtm│ │ │ │Runtm│ │
│ ├─────┤ │ │ ├─────┤ │ │ ├─────┤ │
│ │gRPC │ │ │ │gRPC │ │ │ │gRPC │ │
│ │Srvr │ │ │ │Srvr │ │ │ │Srvr │ │
│ ├─────┤ │ │ ├─────┤ │ │ ├─────┤ │
│ │S&F │ │ │ │S&F │ │ │ │S&F │ │
│ │Engine│ │ │ │Engine│ │ │ │Engine│ │
│ ├─────┤ │ │ ├─────┤ │ │ ├─────┤ │
│ │ExtSys│ │ │ │ExtSys│ │ │ │ExtSys│ │
│ │Gatwy │ │ │ │Gatwy │ │ │ │Gatwy │ │
│ └─────┘ │ │ └─────┘ │ │ └─────┘ │
│ SQLite │ │ SQLite │ │ SQLite │
└─────────┘ └─────────┘ └─────────┘
│ │ │
OPC UA / OPC UA / OPC UA /
Custom Custom Custom
Protocol Protocol Protocol
```
### Site Runtime Actor Hierarchy ### Site Runtime Actor Hierarchy
``` ![Site runtime actor hierarchy](diagrams/site-runtime-actor-hierarchy.png)
Deployment Manager Singleton (Cluster Singleton) <!-- source: diagrams/site-runtime-actor-hierarchy.drawio — edit, then re-export with export-drawio.sh -->
├── Instance Actor (one per deployed, enabled instance)
│ ├── Script Actor (coordinator, one per instance script)
│ │ └── Script Execution Actor (short-lived, per invocation)
│ ├── Alarm Actor (coordinator, one per alarm definition)
│ │ └── Alarm Execution Actor (short-lived, per on-trigger invocation)
│ └── ... (more Script/Alarm Actors)
├── Instance Actor
│ └── ...
└── ... (more Instance Actors)
Site-Wide Akka Stream (attribute + alarm state changes)
├── All Instance Actors publish to the stream
└── Debug view subscribes with instance-level filtering
```
+214
View File
@@ -0,0 +1,214 @@
<mxfile host="app.diagrams.net">
<diagram id="arch-logical" name="Logical Architecture">
<mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1000" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- top: users + load balancer -->
<mxCell id="users" value="Users (Blazor Server)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="430" y="20" width="180" height="40" as="geometry" />
</mxCell>
<mxCell id="lb" value="Load Balancer / Traefik" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="430" y="92" width="180" height="40" as="geometry" />
</mxCell>
<mxCell id="e_users_lb" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="users" target="lb">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_lb_central" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="lb" target="central">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- central cluster container -->
<mxCell id="central" value="CENTRAL CLUSTER — 2-node active / standby" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;fillColor=#eef3fb;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="40" y="160" width="740" height="490" as="geometry" />
</mxCell>
<mxCell id="te" value="Template Engine" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="70" y="206" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="dm" value="Deployment Manager" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="300" y="206" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="ui" value="Central UI (Blazor Server)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="530" y="206" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="sec" value="Security &amp; Auth (JWT / LDAP)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="70" y="270" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="cfg" value="Configuration DB (EF + IAudit)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="300" y="270" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="health" value="Health Monitor" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="530" y="270" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="inapi" value="Inbound API" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="70" y="338" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="extsys" value="External Systems&#10;(X-API-Key)" style="shape=cloud;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="548" y="352" width="184" height="48" as="geometry" />
</mxCell>
<mxCell id="e_ext_in" value="POST /api/{method} · JSON" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;fontSize=10;" edge="1" parent="1" source="extsys" target="inapi">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="mgmt" value="Management Service" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="70" y="402" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="cli" value="CLI&#10;(ClusterClient)" style="shape=cloud;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="548" y="414" width="184" height="48" as="geometry" />
</mxCell>
<mxCell id="e_cli_mgmt" value="ManagementActor + Receptionist" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;fontSize=10;" edge="1" parent="1" source="cli" target="mgmt">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="obslabel" value="Observability / Audit" style="text;html=1;align=left;verticalAlign=middle;fontStyle=2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="70" y="456" width="300" height="18" as="geometry" />
</mxCell>
<mxCell id="ntf" value="Notification Outbox (#21)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="70" y="478" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="sca" value="Site Call Audit (#22)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="300" y="478" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="audit" value="Audit Log (#23)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="530" y="478" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="comm" value="Akka.NET Communication Layer&#10;ClusterClient (command/control) · gRPC Client (real-time streams)" style="whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="70" y="558" width="430" height="64" as="geometry" />
</mxCell>
<mxCell id="mssql" value="MS SQL&#10;Config DB · Machine Data DB" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="580" y="556" width="200" height="70" as="geometry" />
</mxCell>
<mxCell id="e_cfg_sql" value="EF Core" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;dashed=1;fontSize=10;exitX=1;exitY=0.7;entryX=0;entryY=0.4;" edge="1" parent="1" source="cfg" target="mssql">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="515" y="301" />
<mxPoint x="515" y="560" />
</Array>
</mxGeometry>
</mxCell>
<!-- ingests edges -->
<mxCell id="e_ing_ntf" value="ingests (S&amp;F)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;dashed=1;fontSize=9;" edge="1" parent="1" source="comm" target="ntf">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_ing_sca" value="ingests (telemetry)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;dashed=1;fontSize=9;" edge="1" parent="1" source="comm" target="sca">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_ing_audit" value="ingests (telemetry + direct-write)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;dashed=1;fontSize=9;exitX=0.95;exitY=0;entryX=0.1;entryY=1;" edge="1" parent="1" source="comm" target="audit">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- transport annotation between central and sites -->
<mxCell id="transport" value="Akka.NET Remoting (command/control) · gRPC HTTP/2 (real-time data, port 8083)" style="text;html=1;align=center;verticalAlign=middle;fontStyle=2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="120" y="662" width="580" height="28" as="geometry" />
</mxCell>
<!-- SITE A -->
<mxCell id="siteA" value="SITE A — 2-node" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fillColor=#eafaf0;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="40" y="720" width="230" height="364" as="geometry" />
</mxCell>
<mxCell id="a_dcl" value="Data Connection Layer" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="60" y="758" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="a_rt" value="Site Runtime" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="60" y="806" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="a_grpc" value="gRPC Server" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="60" y="854" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="a_snf" value="Store-and-Forward Engine" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="60" y="902" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="a_esg" value="External System Gateway" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="60" y="950" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="a_sql" value="SQLite" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="85" y="1002" width="140" height="64" as="geometry" />
</mxCell>
<!-- SITE B -->
<mxCell id="siteB" value="SITE B — 2-node" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fillColor=#eafaf0;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="295" y="720" width="230" height="364" as="geometry" />
</mxCell>
<mxCell id="b_dcl" value="Data Connection Layer" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="315" y="758" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="b_rt" value="Site Runtime" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="315" y="806" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="b_grpc" value="gRPC Server" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="315" y="854" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="b_snf" value="Store-and-Forward Engine" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="315" y="902" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="b_esg" value="External System Gateway" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="315" y="950" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="b_sql" value="SQLite" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="340" y="1002" width="140" height="64" as="geometry" />
</mxCell>
<!-- SITE N -->
<mxCell id="siteN" value="SITE N — 2-node" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fillColor=#eafaf0;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="550" y="720" width="230" height="364" as="geometry" />
</mxCell>
<mxCell id="n_dcl" value="Data Connection Layer" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="570" y="758" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="n_rt" value="Site Runtime" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="570" y="806" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="n_grpc" value="gRPC Server" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="570" y="854" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="n_snf" value="Store-and-Forward Engine" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="570" y="902" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="n_esg" value="External System Gateway" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="570" y="950" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="n_sql" value="SQLite" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="595" y="1002" width="140" height="64" as="geometry" />
</mxCell>
<!-- central -> sites transport edges -->
<mxCell id="e_c_a" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;strokeWidth=1.5;" edge="1" parent="1" source="comm" target="siteA">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_c_b" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;strokeWidth=1.5;" edge="1" parent="1" source="comm" target="siteB">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_c_n" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;strokeWidth=1.5;" edge="1" parent="1" source="comm" target="siteN">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- field protocol per site -->
<mxCell id="a_proto" value="OPC UA / Custom Protocol" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="60" y="1100" width="190" height="36" as="geometry" />
</mxCell>
<mxCell id="b_proto" value="OPC UA / Custom Protocol" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="315" y="1100" width="190" height="36" as="geometry" />
</mxCell>
<mxCell id="n_proto" value="OPC UA / Custom Protocol" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="570" y="1100" width="190" height="36" as="geometry" />
</mxCell>
<mxCell id="e_a_proto" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="a_dcl" target="a_proto">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_b_proto" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="b_dcl" target="b_proto">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_n_proto" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="n_dcl" target="n_proto">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

@@ -0,0 +1,94 @@
<mxfile host="app.diagrams.net">
<diagram id="actor-hierarchy" name="Site Runtime Actor Hierarchy">
<mxGraphModel dx="1000" dy="700" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="650" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- actor tree -->
<mxCell id="dms" value="Deployment Manager Singleton&#10;(Cluster Singleton)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="20" width="300" height="50" as="geometry" />
</mxCell>
<mxCell id="ia" value="Instance Actor&#10;(one per deployed, enabled instance)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="120" y="120" width="240" height="50" as="geometry" />
</mxCell>
<mxCell id="ia2" value="Instance Actor&#10;( … )" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="430" y="120" width="170" height="50" as="geometry" />
</mxCell>
<mxCell id="moreIA" value="… more Instance Actors" style="whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;dashed=1;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="640" y="125" width="170" height="40" as="geometry" />
</mxCell>
<mxCell id="e_dms_ia" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="dms" target="ia">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_dms_ia2" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="dms" target="ia2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_dms_more" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;dashed=1;" edge="1" parent="1" source="dms" target="moreIA">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="sa" value="Script Actor&#10;(coordinator, one per instance script)" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="40" y="230" width="210" height="50" as="geometry" />
</mxCell>
<mxCell id="aa" value="Alarm Actor&#10;(coordinator, one per alarm definition)" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="270" y="230" width="210" height="50" as="geometry" />
</mxCell>
<mxCell id="more1" value="… more Script /&#10;Alarm Actors" style="whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;dashed=1;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="500" y="235" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="e_ia_sa" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="ia" target="sa">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_ia_aa" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="ia" target="aa">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_ia_more1" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;dashed=1;" edge="1" parent="1" source="ia" target="more1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="sea" value="Script Execution Actor&#10;(short-lived, per invocation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;dashed=1;" vertex="1" parent="1">
<mxGeometry x="40" y="330" width="210" height="50" as="geometry" />
</mxCell>
<mxCell id="aea" value="Alarm Execution Actor&#10;(short-lived, per on-trigger invocation)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;dashed=1;" vertex="1" parent="1">
<mxGeometry x="270" y="330" width="210" height="50" as="geometry" />
</mxCell>
<mxCell id="e_sa_sea" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="sa" target="sea">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_aa_aea" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;" edge="1" parent="1" source="aa" target="aea">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ia2c" value="… (Script / Alarm Actors)" style="whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;dashed=1;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="445" y="330" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="e_ia2_c" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;dashed=1;" edge="1" parent="1" source="ia2" target="ia2c">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- site-wide stream section -->
<mxCell id="streamFrame" value="Site-Wide Akka Stream" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fillColor=#faf5ff;strokeColor=#9673a6;dashed=1;" vertex="1" parent="1">
<mxGeometry x="40" y="440" width="700" height="170" as="geometry" />
</mxCell>
<mxCell id="stream" value="Site-Wide Akka Stream&#10;(attribute + alarm state changes)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="220" y="470" width="340" height="50" as="geometry" />
</mxCell>
<mxCell id="pub" value="All Instance Actors" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="70" y="540" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="debug" value="Debug view&#10;(instance-level filtering)" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="510" y="540" width="200" height="44" as="geometry" />
</mxCell>
<mxCell id="e_pub_stream" value="publish" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;fontSize=10;" edge="1" parent="1" source="pub" target="stream">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e_stream_debug" value="subscribe (filtered)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;fontSize=10;" edge="1" parent="1" source="stream" target="debug">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

@@ -0,0 +1,90 @@
<mxfile host="app.diagrams.net">
<diagram id="topology" name="Topology">
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="800" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Users -->
<mxCell id="users" value="Users&#10;(HTTPS / LB)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="100" width="140" height="60" as="geometry" />
</mxCell>
<!-- Central Cluster container -->
<mxCell id="central" value="Central Cluster" style="swimlane;whiteSpace=wrap;html=1;startSize=30;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="320" y="60" width="380" height="150" as="geometry" />
</mxCell>
<mxCell id="nodeA" value="Node A&#10;Active" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;" vertex="1" parent="central">
<mxGeometry x="40" y="60" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="nodeB" value="Node B&#10;Standby" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=13;fontStyle=1;" vertex="1" parent="central">
<mxGeometry x="220" y="60" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="ab" value="" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;rounded=0;" edge="1" parent="central" source="nodeA" target="nodeB">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Users -> Central -->
<mxCell id="u2c" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="users" target="nodeA">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Sites -->
<mxCell id="site01" value="Site 01" style="swimlane;whiteSpace=wrap;html=1;startSize=26;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="60" y="420" width="220" height="120" as="geometry" />
</mxCell>
<mxCell id="s01a" value="A" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;" vertex="1" parent="site01">
<mxGeometry x="30" y="50" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="s01b" value="B" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=13;fontStyle=1;" vertex="1" parent="site01">
<mxGeometry x="120" y="50" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="site02" value="Site 02" style="swimlane;whiteSpace=wrap;html=1;startSize=26;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="320" y="420" width="220" height="120" as="geometry" />
</mxCell>
<mxCell id="s02a" value="A" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;" vertex="1" parent="site02">
<mxGeometry x="30" y="50" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="s02b" value="B" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=13;fontStyle=1;" vertex="1" parent="site02">
<mxGeometry x="120" y="50" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="site03" value="Site 03" style="swimlane;whiteSpace=wrap;html=1;startSize=26;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="580" y="420" width="220" height="120" as="geometry" />
</mxCell>
<mxCell id="s03a" value="A" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;" vertex="1" parent="site03">
<mxGeometry x="30" y="50" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="s03b" value="B" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=13;fontStyle=1;" vertex="1" parent="site03">
<mxGeometry x="120" y="50" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="siteN" value="Site N" style="swimlane;whiteSpace=wrap;html=1;startSize=26;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;verticalAlign=top;dashed=1;" vertex="1" parent="1">
<mxGeometry x="840" y="420" width="220" height="120" as="geometry" />
</mxCell>
<mxCell id="sNa" value="A" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;" vertex="1" parent="siteN">
<mxGeometry x="30" y="50" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="sNb" value="B" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=13;fontStyle=1;" vertex="1" parent="siteN">
<mxGeometry x="120" y="50" width="70" height="50" as="geometry" />
</mxCell>
<!-- Central -> Sites (fan-out from cluster bottom) -->
<mxCell id="c2s01" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="central" target="site01">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="c2s02" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="central" target="site02">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="c2s03" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="central" target="site03">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="c2sN" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="central" target="siteN">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+2 -18
View File
@@ -6,24 +6,8 @@ ScadaBridge uses a hub-and-spoke architecture:
- **Central Cluster**: Two-node active/standby Akka.NET cluster for management, UI, and coordination. - **Central Cluster**: Two-node active/standby Akka.NET cluster for management, UI, and coordination.
- **Site Clusters**: Two-node active/standby Akka.NET clusters at each remote site for data collection and local processing. - **Site Clusters**: Two-node active/standby Akka.NET clusters at each remote site for data collection and local processing.
``` ![topology-architecture-overview](diagrams/topology-architecture-overview.png)
┌──────────────────────────┐ <!-- source: diagrams/topology-architecture-overview.drawio — edit, then re-export with export-drawio.sh -->
│ Central Cluster │
│ ┌──────┐ ┌──────┐ │
Users ──────────► │ │Node A│◄──►│Node B│ │
(HTTPS/LB) │ │Active│ │Stby │ │
│ └──┬───┘ └──┬───┘ │
└─────┼───────────┼────────┘
│ │
┌───────────┼───────────┼───────────┐
│ │ │ │
┌─────▼─────┐ ┌──▼──────┐ ┌──▼──────┐ ┌──▼──────┐
│ Site 01 │ │ Site 02 │ │ Site 03 │ │ Site N │
│ ┌──┐ ┌──┐ │ │ ┌──┐┌──┐│ │ ┌──┐┌──┐│ │ ┌──┐┌──┐│
│ │A │ │B │ │ │ │A ││B ││ │ │A ││B ││ │ │A ││B ││
│ └──┘ └──┘ │ │ └──┘└──┘│ │ └──┘└──┘│ │ └──┘└──┘│
└───────────┘ └─────────┘ └─────────┘ └─────────┘
```
## Central Cluster Setup ## Central Cluster Setup
@@ -39,28 +39,8 @@ Both endpoints use the same `Protocol`. EF Core migration renames `Configuration
The `DataConnectionActor` Reconnecting state is extended: The `DataConnectionActor` Reconnecting state is extended:
``` ![primary-backup-failover-state-machine](diagrams/primary-backup-failover-state-machine.png)
Connected <!-- source: diagrams/primary-backup-failover-state-machine.drawio — edit, then re-export with export-drawio.sh -->
│ disconnect detected
Push bad quality to all subscribers
Retry active endpoint (5s interval)
│ failure
_consecutiveFailures++
├─ < FailoverRetryCount → retry same endpoint
├─ ≥ FailoverRetryCount AND backup exists
│ → dispose adapter, switch _activeEndpoint, reset counter
│ → create fresh adapter with other config
│ → attempt connect
└─ ≥ FailoverRetryCount AND no backup
→ keep retrying indefinitely (current behavior)
```
**On successful reconnect (either endpoint):** **On successful reconnect (either endpoint):**
1. Reset `_consecutiveFailures = 0` 1. Reset `_consecutiveFailures = 0`
@@ -32,34 +32,8 @@ We want a strongly-typed model for OPC UA endpoint configuration, a validator th
## Architecture ## Architecture
``` ![opcua-config-model-architecture](diagrams/opcua-config-model-architecture.png)
┌──────────────────────────────────────┐ <!-- source: diagrams/opcua-config-model-architecture.drawio — edit, then re-export with export-drawio.sh -->
│ ZB.MOM.WW.ScadaBridge.Commons │
│ Types/DataConnections/ │
│ OpcUaEndpointConfig.cs (POCO) │
│ OpcUaHeartbeatConfig.cs (POCO) │
│ OpcUaSecurityMode.cs (enum) │
│ Validators/ │
│ OpcUaEndpointConfigValidator.cs │
│ Serialization/ │
│ OpcUaEndpointConfigSerializer.cs │
└──────────────────────────────────────┘
│ (referenced by both)
┌───────┴────────────────────────┐
▼ ▼
┌──────────────────────────┐ ┌────────────────────────────┐
│ ZB.MOM.WW.ScadaBridge.CentralUI │ │ ZB.MOM.WW.ScadaBridge.SiteRuntime │
│ Components/Forms/ │ │ Actors/ │
│ OpcUaEndpointEditor │ │ DeploymentManagerActor │
│ .razor (shared) │ │ (passes raw JSON to │
│ │ │ DataConnectionFactory)│
│ Pages/Admin/ │ │ │
│ DataConnectionForm │ │ DataConnections.OpcUa/ │
│ .razor │ │ OpcUaDataConnection.cs │
└──────────────────────────┘ │ (consumes typed model) │
└────────────────────────────┘
```
Both sides deserialize from `DataConnection.PrimaryConfiguration` / `BackupConfiguration` strings into the same `OpcUaEndpointConfig` instance. The DB column type does not change. Both sides deserialize from `DataConnection.PrimaryConfiguration` / `BackupConfiguration` strings into the same `OpcUaEndpointConfig` instance. The DB column type does not change.
@@ -17,29 +17,8 @@ A sibling `docker-env2/` directory with `deploy.sh` / `teardown.sh` / `seed-site
## Architecture Overview ## Architecture Overview
``` ![env2-architecture-overview](diagrams/env2-architecture-overview.png)
(host machine) <!-- source: diagrams/env2-architecture-overview.drawio — edit, then re-export with export-drawio.sh -->
Primary stack (already existing — unchanged) Env2 stack (new)
┌────────────────────────────────────┐ ┌──────────────────────────────┐
│ Traefik :9000 ◄── 9001/9002 UI │ │ Traefik :9100 ◄── 9101/9102 UI│
│ Central A/B (9011/9012 Akka) │ │ Central A/B (9111/9112 Akka) │
│ Site-A/B/C (9021..9044) │ │ Site-X (9121/9122 Akka, │
└─────────────┬──────────────────────┘ │ 9123/9124 gRPC) │
│ └──────────┬───────────────────┘
│ │
▼ scadabridge-net (shared bridge network) ◄──────┘
┌──────────────────────────────────────────────────────────────┐
│ scadabridge-mssql ScadaBridgeConfig (primary DB) │
│ ScadaBridgeMachineData (primary DB) │
│ ScadaBridgeConfig2 (env2 DB) ← new │
│ ScadaBridgeMachineData2(env2 DB) ← new │
│ scadabridge-ldap (shared — same test users) │
│ scadabridge-smtp (shared Mailpit) │
│ scadabridge-opcua (shared) │
│ scadabridge-restapi (shared) │
└──────────────────────────────────────────────────────────────┘
```
Both stacks attach to the same `scadabridge-net` Docker bridge so env2's app containers can reach the infra services by container hostname (`scadabridge-mssql`, `scadabridge-ldap`, etc.). Akka clusters are independent — each side's `SeedNodes` lists only its own central nodes, so they never gossip-merge despite sharing the network. Both stacks attach to the same `scadabridge-net` Docker bridge so env2's app containers can reach the infra services by container hostname (`scadabridge-mssql`, `scadabridge-ldap`, etc.). Akka clusters are independent — each side's `SeedNodes` lists only its own central nodes, so they never gossip-merge despite sharing the network.
+2 -13
View File
@@ -14,19 +14,8 @@
## Task Dependency Graph ## Task Dependency Graph
``` ![env2-task-dependency-graph](diagrams/env2-task-dependency-graph.png)
T0 ─┐ ┐ <!-- source: diagrams/env2-task-dependency-graph.drawio — edit, then re-export with export-drawio.sh -->
T1 ─┤ (all independent, all │
T2 ─┤ parallelizable, all ├─► T10 (manual smoke test)
T3 ─┤ ready from the start) │
T4 ─┤ │
T6 ─┤ │
T7 ─┤ │
T8 ─┤ │
T9 ─┘ │
T0,T4 ──► T5 (lifecycle scripts) ─────────┘
```
T10 is the only task that requires all of T0T9 done. Everything else can run in parallel. T10 is the only task that requires all of T0T9 done. Everything else can run in parallel.
@@ -18,27 +18,8 @@
## Section 1 — Architecture ## Section 1 — Architecture
``` ![opcua-tag-browser-architecture](diagrams/opcua-tag-browser-architecture.png)
[Blazor Server browser] <!-- source: diagrams/opcua-tag-browser-architecture.drawio — edit, then re-export with export-drawio.sh -->
│ SignalR
[CentralUI: InstanceConfigure.razor]
│ opens
[CentralUI: <OpcUaBrowserDialog/>]
│ uses
[CentralUI: IOpcUaBrowseService] ── implementation calls
[CommunicationService.SendCommandToSiteAsync<BrowseOpcUaNodeResult>(siteId, BrowseOpcUaNodeCommand)]
│ ClusterClient Ask, ManagementEnvelope { User, Command, CorrelationId }
[Site: CentralCommunicationActor → DataConnectionManagerActor]
│ dispatches to IBrowsableDataConnection (RealOpcUaClient)
[OPC UA server] ◄── OPC Foundation .NET SDK Browse service
```
Three slices, top-to-bottom: Three slices, top-to-bottom:
1. **Data model.** Add a per-instance OPC UA address override (new column on `InstanceConnectionBinding`). 1. **Data model.** Add a per-instance OPC UA address override (new column on `InstanceConnectionBinding`).
@@ -164,25 +145,8 @@ Returning failure inside `BrowseOpcUaNodeResult` (rather than exceptions across
**Wire flow.** **Wire flow.**
``` ![opcua-tag-browser-command-flow](diagrams/opcua-tag-browser-command-flow.png)
CentralUI.OpcUaBrowseService.BrowseChildrenAsync(siteId, connId, parent) <!-- source: diagrams/opcua-tag-browser-command-flow.drawio — edit, then re-export with export-drawio.sh -->
→ CommunicationService.SendCommandToSiteAsync<BrowseOpcUaNodeResult>(
siteId,
new BrowseOpcUaNodeCommand(connId, parent))
→ ManagementEnvelope { User, Command, CorrelationId } over ClusterClient
→ Site: CentralCommunicationActor unwraps envelope
→ Site: DataConnectionManagerActor receives BrowseOpcUaNodeCommand
- Look up IDataConnection by Id
- if not found → ConnectionNotFound
- if !(conn is IBrowsableDataConnection) → NotBrowsable
- else await conn.BrowseChildrenAsync(ParentNodeId, ct)
- Catch ConnectionNotConnectedException → ConnectionNotConnected
- Catch OperationCanceledException → Timeout
- Catch ServiceResultException → ServerError + verbatim msg
- Else success: BrowseOpcUaNodeResult(children, truncated, null)
→ Reply travels back via CentralCommunicationActor → CommunicationService
→ returned to CentralUI page
```
Handler lives in the **DCL coordinator actor** (the same actor that owns the per-connection `IDataConnection` instances) — keeps lifecycle and browse co-located so we don't race against reconnect. Handler lives in the **DCL coordinator actor** (the same actor that owns the per-connection `IDataConnection` instances) — keeps lifecycle and browse co-located so we don't race against reconnect.
+2 -17
View File
@@ -16,23 +16,8 @@
## Task dependency overview ## Task dependency overview
``` ![native-alarms-task-dependency](diagrams/native-alarms-task-dependency.png)
T1 ─┬─ T2 ─┬─ T17 (computed AlarmActor enrich) <!-- source: diagrams/native-alarms-task-dependency.drawio — edit, then re-export with export-drawio.sh -->
│ ├─ T18 (proto) ── T19 (grpc mapping) ── T23 (DebugView)
T3 ─┼─ T10 (DCL actor)
├─ T11 (OPC UA adapter)
└─ T12 (MxGateway adapter)
T4 ─┬─ T5 ── T6 ── T21 (mgmt handlers)
├─ T7 (migration)
├─ T8 ── T9 (validation)
└─ T20 ─┬─ T21 ── T26 (seed)
├─ T22 (CLI)
├─ T24 (template UI)
└─ T25 (instance UI)
T13, T14 ──┐
T1,T2,T3,T4(Resolved),T13,T14 ── T15 (NativeAlarmActor) ── T16 (InstanceActor wiring)
(everything) ── T27 (docs) , T28 (integration/manual verify)
```
--- ---
@@ -0,0 +1,96 @@
<mxfile host="app.diagrams.net">
<diagram id="env2arch" name="Env2 Architecture">
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Host machine container -->
<mxCell id="host" value="host machine" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#666666;verticalAlign=top;fontStyle=2;fontColor=#666666;dashed=1;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="1020" height="800" as="geometry" />
</mxCell>
<!-- Primary stack -->
<mxCell id="primary" value="Primary stack&#10;(already existing — unchanged)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;verticalAlign=top;fontStyle=1;align=center;spacingTop=6;" vertex="1" parent="1">
<mxGeometry x="80" y="100" width="420" height="220" as="geometry" />
</mxCell>
<mxCell id="p-traefik" value="Traefik :9000 ◄── 9001/9002 UI" style="whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="100" y="160" width="380" height="40" as="geometry" />
</mxCell>
<mxCell id="p-central" value="Central A/B (9011/9012 Akka)" style="whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="100" y="210" width="380" height="40" as="geometry" />
</mxCell>
<mxCell id="p-site" value="Site-A/B/C (9021..9044)" style="whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="100" y="260" width="380" height="40" as="geometry" />
</mxCell>
<!-- Env2 stack -->
<mxCell id="env2" value="Env2 stack&#10;(new)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;fontStyle=1;align=center;spacingTop=6;" vertex="1" parent="1">
<mxGeometry x="600" y="100" width="420" height="220" as="geometry" />
</mxCell>
<mxCell id="e-traefik" value="Traefik :9100 ◄── 9101/9102 UI" style="whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#82b366;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="620" y="160" width="380" height="40" as="geometry" />
</mxCell>
<mxCell id="e-central" value="Central A/B (9111/9112 Akka)" style="whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#82b366;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="620" y="210" width="380" height="40" as="geometry" />
</mxCell>
<mxCell id="e-site" value="Site-X (9121/9122 Akka, 9123/9124 gRPC)" style="whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#82b366;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="620" y="260" width="380" height="40" as="geometry" />
</mxCell>
<!-- Shared network bar -->
<mxCell id="net" value="scadabridge-net (shared bridge network)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="400" width="940" height="50" as="geometry" />
</mxCell>
<!-- Infra container -->
<mxCell id="infra" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="120" y="500" width="860" height="300" as="geometry" />
</mxCell>
<!-- MSSQL block -->
<mxCell id="mssql" value="scadabridge-mssql" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;verticalAlign=top;fontStyle=1;spacingTop=4;" vertex="1" parent="1">
<mxGeometry x="150" y="530" width="160" height="240" as="geometry" />
</mxCell>
<mxCell id="db1" value="ScadaBridgeConfig (primary DB)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="340" y="540" width="300" height="40" as="geometry" />
</mxCell>
<mxCell id="db2" value="ScadaBridgeMachineData (primary DB)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="340" y="590" width="300" height="40" as="geometry" />
</mxCell>
<mxCell id="db3" value="ScadaBridgeConfig2 (env2 DB) ← new" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="340" y="640" width="300" height="40" as="geometry" />
</mxCell>
<mxCell id="db4" value="ScadaBridgeMachineData2 (env2 DB) ← new" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="340" y="690" width="300" height="40" as="geometry" />
</mxCell>
<!-- Shared commodity infra services -->
<mxCell id="ldap" value="scadabridge-ldap&#10;(shared — same test users)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="680" y="530" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="smtp" value="scadabridge-smtp&#10;(shared Mailpit)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="680" y="580" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="opcua" value="scadabridge-opcua (shared)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="680" y="630" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="restapi" value="scadabridge-restapi (shared)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="680" y="680" width="280" height="40" as="geometry" />
</mxCell>
<!-- Edges: primary -> net, env2 -> net -->
<mxCell id="ep" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="primary" target="net">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="ee" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="env2" target="net">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- net -> infra -->
<mxCell id="eni" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#d6b656;" edge="1" parent="1" source="net" target="infra">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

@@ -0,0 +1,98 @@
<mxfile host="app.diagrams.net">
<diagram id="env2dag" name="Env2 Task Dependency Graph">
<mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1000" pageHeight="800" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Independent / parallelizable group label -->
<mxCell id="grpLabel" value="all independent, all parallelizable, all ready from the start" style="text;html=1;align=center;verticalAlign=middle;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="40" y="30" width="300" height="30" as="geometry" />
</mxCell>
<!-- Independent task nodes (T0..T9 minus T5) -->
<mxCell id="t0" value="T0" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="80" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t1" value="T1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="130" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t2" value="T2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="180" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t3" value="T3" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="230" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t4" value="T4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="280" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t6" value="T6" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="330" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t7" value="T7" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="380" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t8" value="T8" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="430" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="t9" value="T9" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="480" width="80" height="40" as="geometry" />
</mxCell>
<!-- T5: lifecycle scripts (fed by T0 and T4) -->
<mxCell id="t5" value="T5&#10;lifecycle scripts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="440" y="430" width="160" height="50" as="geometry" />
</mxCell>
<!-- T10: manual smoke test -->
<mxCell id="t10" value="T10&#10;manual smoke test" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="780" y="255" width="160" height="60" as="geometry" />
</mxCell>
<!-- Edges: each independent task -> T10 -->
<mxCell id="e0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t0" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t1" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t2" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t3" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t6" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t7" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t8" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="t9" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- T0,T4 -> T5 -->
<mxCell id="e05" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#d79b00;" edge="1" parent="1" source="t0" target="t5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#d79b00;" edge="1" parent="1" source="t4" target="t5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- T5 -> T10 -->
<mxCell id="e510" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#d79b00;" edge="1" parent="1" source="t5" target="t10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Note -->
<mxCell id="note" value="T10 is the only task that requires all of T0T9 done. Everything else runs in parallel." style="text;html=1;align=left;verticalAlign=middle;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="640" y="360" width="320" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

@@ -0,0 +1,41 @@
<mxfile host="app.diagrams.net">
<diagram id="perwp" name="Per-Work-Package Execution">
<mxGraphModel dx="900" dy="1100" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="700" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="s1" value="1. READ the WP description and acceptance criteria" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;verticalAlign=middle;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="180" y="40" width="380" height="50" as="geometry" />
</mxCell>
<mxCell id="s2" value="2. READ all traced requirements (HLR bullets, KDD, CD constraints) to understand intent" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;verticalAlign=middle;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="180" y="130" width="380" height="60" as="geometry" />
</mxCell>
<mxCell id="s3" value="3. IMPLEMENT the WP&#10;&#8226; Write code&#10;&#8226; Write unit tests for acceptance criteria&#10;&#8226; Write negative tests for prohibition criteria" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=12;verticalAlign=middle;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="180" y="230" width="380" height="100" as="geometry" />
</mxCell>
<mxCell id="s4" value="4. VERIFY acceptance criteria&#10;&#8226; Run tests: all must pass&#10;&#8226; Walk each acceptance criterion line by line&#10;&#8226; If a criterion cannot be verified yet (depends on a later WP), note it as &quot;deferred to WP-N&quot;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;verticalAlign=middle;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="180" y="370" width="380" height="120" as="geometry" />
</mxCell>
<mxCell id="s5" value="5. UPDATE the phase execution checklist&#10;&#8226; Mark WP as complete with date&#10;&#8226; Note any deferred criteria&#10;&#8226; Note any questions logged" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;verticalAlign=middle;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="180" y="530" width="380" height="100" as="geometry" />
</mxCell>
<mxCell id="s6" value="6. COMMIT with message:&#10;&quot;Phase N WP-M: &lt;summary&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=12;verticalAlign=middle;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="180" y="670" width="380" height="60" as="geometry" />
</mxCell>
<!-- Edges -->
<mxCell id="e12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s1" target="s2"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s2" target="s3"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e34" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s3" target="s4"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s4" target="s5"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e56" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s5" target="s6"><mxGeometry relative="1" as="geometry" /></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

@@ -0,0 +1,41 @@
<mxfile host="app.diagrams.net">
<diagram id="channelbridge" name="Channel Bridging">
<mxGraphModel dx="1000" dy="700" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="640" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Producer side: Akka Actor Thread(s) -->
<mxCell id="akka" value="Akka Actor Thread(s)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="80" y="60" width="240" height="60" as="geometry" />
</mxCell>
<!-- Consumer side: gRPC Response Stream -->
<mxCell id="grpc" value="gRPC Response Stream" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="560" y="60" width="240" height="60" as="geometry" />
</mxCell>
<!-- Channel box -->
<mxCell id="channel" value="Channel&lt;SiteStreamEvent&gt;&#10;&#10;BoundedChannelOptions(1000)&#10;FullMode = DropOldest" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="240" y="280" width="400" height="120" as="geometry" />
</mxCell>
<!-- Producer writes into channel -->
<mxCell id="write" value="channel.Writer.TryWrite(evt)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;fontSize=12;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="akka" target="channel">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Channel read out to gRPC stream -->
<mxCell id="read" value="await responseStream.WriteAsync(evt)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;fontSize=12;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="channel" target="grpc">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Notes -->
<mxCell id="note" value="• Bounded capacity (1000): prevents unbounded memory growth if the gRPC client is slow&#10;• DropOldest: matches the existing SiteStreamManager overflow strategy&#10;• ReadAllAsync: yields items as they arrive, naturally async" style="text;html=1;align=left;verticalAlign=middle;fontSize=11;fontColor=#666666;spacingLeft=4;" vertex="1" parent="1">
<mxGeometry x="80" y="450" width="720" height="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

@@ -0,0 +1,83 @@
<mxfile host="app.diagrams.net">
<diagram id="statemachine" name="Reconnection State Machine">
<mxGraphModel dx="1000" dy="800" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="820" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Streaming state -->
<mxCell id="streaming" value="Streaming" style="rounded=1;arcSize=30;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="360" y="60" width="180" height="60" as="geometry" />
</mxCell>
<mxCell id="streamingNote" value="Normal state:&#10;gRPC stream active" style="text;html=1;align=left;verticalAlign=middle;fontSize=11;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="560" y="70" width="170" height="40" as="geometry" />
</mxCell>
<!-- Reconnecting state -->
<mxCell id="reconnecting" value="Reconnecting" style="rounded=1;arcSize=30;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="360" y="240" width="180" height="60" as="geometry" />
</mxCell>
<mxCell id="reconnectingNote" value="try other node endpoint" style="text;html=1;align=left;verticalAlign=middle;fontSize=11;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="560" y="250" width="170" height="40" as="geometry" />
</mxCell>
<!-- Decision diamond -->
<mxCell id="decide" value="reconnect&#10;result?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="380" y="400" width="140" height="90" as="geometry" />
</mxCell>
<!-- Schedule retry box -->
<mxCell id="retry" value="schedule retry&#10;(5s backoff)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="100" y="410" width="170" height="60" as="geometry" />
</mxCell>
<!-- Terminated state -->
<mxCell id="terminated" value="Terminated" style="rounded=1;arcSize=30;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="360" y="620" width="180" height="60" as="geometry" />
</mxCell>
<mxCell id="terminatedNote" value="notify consumer,&#10;stop actor" style="text;html=1;align=left;verticalAlign=middle;fontSize=11;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="560" y="630" width="170" height="40" as="geometry" />
</mxCell>
<!-- Transitions -->
<mxCell id="t_stream_recon" value="gRPC stream error /&#10;keepalive timeout" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;fontSize=11;" edge="1" parent="1" source="streaming" target="reconnecting">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="t_recon_decide" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;" edge="1" parent="1" source="reconnecting" target="decide">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- success -> Streaming -->
<mxCell id="t_success" value="success" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;fontSize=11;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="decide" target="streaming">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="700" y="445" />
<mxPoint x="700" y="90" />
</Array>
</mxGeometry>
</mxCell>
<!-- failure (retry < max) -> schedule retry -->
<mxCell id="t_retry" value="failure&#10;(retry &lt; max)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;fontSize=11;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="decide" target="retry">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- schedule retry -> Reconnecting (loop back) -->
<mxCell id="t_loop" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="retry" target="reconnecting">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="185" y="270" />
</Array>
</mxGeometry>
</mxCell>
<!-- failure (retry >= max) -> Terminated -->
<mxCell id="t_term" value="failure&#10;(retry &gt;= max)" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;fontSize=11;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="decide" target="terminated">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

@@ -0,0 +1,102 @@
<mxfile host="app.diagrams.net">
<diagram id="grpcarch" name="gRPC Streams Architecture">
<mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="1000" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Column headers -->
<mxCell id="hdrCentral" value="Central Cluster" style="text;html=1;align=center;verticalAlign=middle;fontSize=16;fontStyle=1;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="120" y="30" width="280" height="30" as="geometry" />
</mxCell>
<mxCell id="hdrSite" value="Site Cluster" style="text;html=1;align=center;verticalAlign=middle;fontSize=16;fontStyle=1;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="680" y="30" width="280" height="30" as="geometry" />
</mxCell>
<mxCell id="ruleC" value="" style="line;strokeWidth=2;html=1;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="120" y="60" width="280" height="10" as="geometry" />
</mxCell>
<mxCell id="ruleS" value="" style="line;strokeWidth=2;html=1;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="680" y="60" width="280" height="10" as="geometry" />
</mxCell>
<!-- CENTRAL column -->
<mxCell id="bridgeTop" value="DebugStreamBridgeActor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="120" y="110" width="280" height="50" as="geometry" />
</mxCell>
<!-- SITE column -->
<mxCell id="instance" value="InstanceActor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="680" y="110" width="280" height="50" as="geometry" />
</mxCell>
<!-- ClusterClient command/control between bridge and instance -->
<mxCell id="ccSub" value="SubscribeDebugView &#9658;" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;dashed=1;fontSize=11;exitX=1;exitY=0.3;exitDx=0;exitDy=0;entryX=0;entryY=0.3;entryDx=0;entryDy=0;verticalAlign=bottom;labelBackgroundColor=#ffffff;" edge="1" parent="1" source="bridgeTop" target="instance">
<mxGeometry relative="1" as="geometry">
<mxPoint x="0" y="-8" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ccSnap" value="&#9668; DebugViewSnapshot" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;dashed=1;fontSize=11;exitX=0;exitY=0.7;exitDx=0;exitDy=0;entryX=1;entryY=0.7;entryDx=0;entryDy=0;verticalAlign=top;labelBackgroundColor=#ffffff;" edge="1" parent="1" source="instance" target="bridgeTop">
<mxGeometry relative="1" as="geometry">
<mxPoint x="0" y="8" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="ccLabel" value="ClusterClient:&#10;command / control" style="text;html=1;align=center;verticalAlign=middle;fontSize=10;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="455" y="178" width="170" height="34" as="geometry" />
</mxCell>
<!-- Instance publishes -->
<mxCell id="publishes" value="publishes&#10;AttributeValueChanged&#10;AlarmStateChanged" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="680" y="220" width="280" height="60" as="geometry" />
</mxCell>
<mxCell id="instPub" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;" edge="1" parent="1" source="instance" target="publishes">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- gRPC server (site) -->
<mxCell id="grpcServer" value="SiteStreamGrpcServer&#10;(Kestrel, on site)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="680" y="340" width="280" height="60" as="geometry" />
</mxCell>
<mxCell id="pub2srv" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;" edge="1" parent="1" source="publishes" target="grpcServer">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="srvNote1" value="receives from&#10;SiteStreamManager" style="text;html=1;align=left;verticalAlign=middle;fontSize=10;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="970" y="345" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="srvNote2" value="filters by&#10;instance name" style="text;html=1;align=left;verticalAlign=middle;fontSize=10;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="970" y="375" width="120" height="30" as="geometry" />
</mxCell>
<!-- gRPC client (central) -->
<mxCell id="grpcClient" value="SiteStreamGrpcClient&#10;(per-site, on central)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="120" y="340" width="280" height="60" as="geometry" />
</mxCell>
<!-- gRPC stream: server -> client -->
<mxCell id="grpcStream" value="gRPC stream (HTTP/2)" style="edgeStyle=none;html=1;endArrow=block;rounded=0;strokeWidth=2;strokeColor=#82b366;fontSize=12;fontStyle=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="grpcServer" target="grpcClient">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="cliNote" value="reads from gRPC stream&#10;routes by correlationId" style="text;html=1;align=center;verticalAlign=middle;fontSize=10;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="120" y="410" width="280" height="30" as="geometry" />
</mxCell>
<!-- bridge (central, lower) -->
<mxCell id="bridgeBottom" value="DebugStreamBridgeActor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="120" y="460" width="280" height="50" as="geometry" />
</mxCell>
<mxCell id="cli2bridge" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;" edge="1" parent="1" source="grpcClient" target="bridgeBottom">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- SignalR / Blazor -->
<mxCell id="signalr" value="SignalR Hub / Blazor UI" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="120" y="560" width="280" height="50" as="geometry" />
</mxCell>
<mxCell id="bridge2signalr" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=block;rounded=0;" edge="1" parent="1" source="bridgeBottom" target="signalr">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

@@ -0,0 +1,158 @@
<mxfile host="app.diagrams.net">
<diagram id="nadag" name="Native Alarms Task Dependency">
<mxGraphModel dx="1600" dy="1100" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1500" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- ============ Block 1: T1/T3 sources ============ -->
<mxCell id="t1" value="T1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="60" width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="t3" value="T3" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="200" width="90" height="40" as="geometry" />
</mxCell>
<!-- T2 and the DCL/adapter fan-out from T1+T3 -->
<mxCell id="t2" value="T2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="240" y="60" width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="t10" value="T10&#10;DCL actor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="240" y="180" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="t11" value="T11&#10;OPC UA adapter" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="240" y="240" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="t12" value="T12&#10;MxGateway adapter" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="240" y="300" width="140" height="40" as="geometry" />
</mxCell>
<!-- T2 fan-out: T17, T18->T19->T23 -->
<mxCell id="t17" value="T17&#10;computed AlarmActor enrich" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="460" y="20" width="180" height="50" as="geometry" />
</mxCell>
<mxCell id="t18" value="T18&#10;proto" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="460" y="100" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="t19" value="T19&#10;grpc mapping" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="680" y="100" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="t23" value="T23&#10;DebugView" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="900" y="100" width="140" height="40" as="geometry" />
</mxCell>
<!-- ============ Block 2: T4 source ============ -->
<mxCell id="t4" value="T4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="500" width="90" height="40" as="geometry" />
</mxCell>
<!-- T4 fan-out -->
<mxCell id="t5" value="T5" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="240" y="420" width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="t6" value="T6" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="400" y="420" width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="t7" value="T7&#10;migration" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="240" y="480" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="t8" value="T8" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="240" y="540" width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="t9" value="T9&#10;validation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="400" y="540" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="t20" value="T20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="240" y="640" width="90" height="40" as="geometry" />
</mxCell>
<!-- T21: fed by both T6 and T20 -->
<mxCell id="t21" value="T21&#10;mgmt handlers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="600" y="480" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="t26" value="T26&#10;seed" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="820" y="480" width="120" height="40" as="geometry" />
</mxCell>
<!-- T20 fan-out (others) -->
<mxCell id="t22" value="T22&#10;CLI" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="460" y="620" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="t24" value="T24&#10;template UI" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="460" y="680" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="t25" value="T25&#10;instance UI" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="460" y="740" width="120" height="40" as="geometry" />
</mxCell>
<!-- ============ Block 3: T13/T14 -> T15 -> T16 ============ -->
<mxCell id="t13" value="T13" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="860" width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="t14" value="T14" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="920" width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="t15" value="T15&#10;NativeAlarmActor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="600" y="880" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="t16" value="T16&#10;InstanceActor wiring" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="820" y="880" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="t15in" value="inputs to T15:&#10;T1, T2, T3, T4 (Resolved), T13, T14" style="text;html=1;align=left;verticalAlign=middle;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="180" y="880" width="380" height="50" as="geometry" />
</mxCell>
<!-- ============ Block 4: everything -> T27, T28 ============ -->
<mxCell id="t27" value="T27&#10;docs" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="1060" y="980" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="t28" value="T28&#10;integration / manual verify" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="1060" y="1040" width="200" height="40" as="geometry" />
</mxCell>
<mxCell id="evt" value="(everything) ──►" style="text;html=1;align=right;verticalAlign=middle;fontStyle=2;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="880" y="990" width="170" height="40" as="geometry" />
</mxCell>
<!-- ============ Edges ============ -->
<!-- T1 + T3 -> T2, T10, T11, T12 -->
<mxCell id="b1t2a" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t1" target="t2"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="b1t10a" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t1" target="t10"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="b1t11a" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t1" target="t11"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="b1t12a" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t1" target="t12"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="b1t2b" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t3" target="t2"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="b1t10b" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t3" target="t10"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="b1t11b" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t3" target="t11"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="b1t12b" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#6c8ebf;" edge="1" parent="1" source="t3" target="t12"><mxGeometry relative="1" as="geometry" /></mxCell>
<!-- T2 -> T17, T18; T18 -> T19 -> T23 -->
<mxCell id="e_t2_17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#9673a6;" edge="1" parent="1" source="t2" target="t17"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_t2_18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#9673a6;" edge="1" parent="1" source="t2" target="t18"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_18_19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#9673a6;" edge="1" parent="1" source="t18" target="t19"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_19_23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#9673a6;" edge="1" parent="1" source="t19" target="t23"><mxGeometry relative="1" as="geometry" /></mxCell>
<!-- T4 -> T5, T7, T8, T20 -->
<mxCell id="e_4_5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t4" target="t5"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_4_7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t4" target="t7"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_4_8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t4" target="t8"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_4_20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t4" target="t20"><mxGeometry relative="1" as="geometry" /></mxCell>
<!-- T5 -> T6 -> T21; T8 -> T9 -->
<mxCell id="e_5_6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t5" target="t6"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_6_21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t6" target="t21"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_8_9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t8" target="t9"><mxGeometry relative="1" as="geometry" /></mxCell>
<!-- T20 -> T21, T22, T24, T25; T21 -> T26 -->
<mxCell id="e_20_21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t20" target="t21"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_20_22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t20" target="t22"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_20_24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t20" target="t24"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_20_25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t20" target="t25"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_21_26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#82b366;" edge="1" parent="1" source="t21" target="t26"><mxGeometry relative="1" as="geometry" /></mxCell>
<!-- T13, T14 -> T15 -> T16 -->
<mxCell id="e_13_15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#d79b00;" edge="1" parent="1" source="t13" target="t15"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_14_15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#d79b00;" edge="1" parent="1" source="t14" target="t15"><mxGeometry relative="1" as="geometry" /></mxCell>
<mxCell id="e_15_16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;strokeColor=#d79b00;" edge="1" parent="1" source="t15" target="t16"><mxGeometry relative="1" as="geometry" /></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

@@ -0,0 +1,105 @@
<mxfile host="app.diagrams.net">
<diagram id="arch" name="Config Model Architecture">
<mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Commons container -->
<mxCell id="commons" value="ZB.MOM.WW.ScadaBridge.Commons"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;fontStyle=1;verticalAlign=top;spacingTop=8;"
vertex="1" parent="1">
<mxGeometry x="360" y="40" width="380" height="270" as="geometry" />
</mxCell>
<!-- Types/DataConnections group -->
<mxCell id="types" value="Types/DataConnections/&#10;&#10;OpcUaEndpointConfig.cs (POCO)&#10;OpcUaHeartbeatConfig.cs (POCO)&#10;OpcUaSecurityMode.cs (enum)"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="380" y="80" width="340" height="100" as="geometry" />
</mxCell>
<!-- Validators group -->
<mxCell id="validators" value="Validators/&#10;OpcUaEndpointConfigValidator.cs"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="380" y="190" width="340" height="50" as="geometry" />
</mxCell>
<!-- Serialization group -->
<mxCell id="serialization" value="Serialization/&#10;OpcUaEndpointConfigSerializer.cs"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="380" y="250" width="340" height="50" as="geometry" />
</mxCell>
<!-- CentralUI container -->
<mxCell id="centralui" value="ZB.MOM.WW.ScadaBridge.CentralUI"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;fontStyle=1;verticalAlign=top;spacingTop=8;"
vertex="1" parent="1">
<mxGeometry x="120" y="480" width="320" height="220" as="geometry" />
</mxCell>
<mxCell id="cui_forms" value="Components/Forms/&#10;OpcUaEndpointEditor.razor (shared)"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="140" y="520" width="280" height="70" as="geometry" />
</mxCell>
<mxCell id="cui_pages" value="Pages/Admin/&#10;DataConnectionForm.razor"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="140" y="600" width="280" height="70" as="geometry" />
</mxCell>
<!-- SiteRuntime container -->
<mxCell id="siteruntime" value="ZB.MOM.WW.ScadaBridge.SiteRuntime"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;fontStyle=1;verticalAlign=top;spacingTop=8;"
vertex="1" parent="1">
<mxGeometry x="660" y="480" width="340" height="220" as="geometry" />
</mxCell>
<mxCell id="sr_actors" value="Actors/&#10;DeploymentManagerActor&#10;(passes raw JSON to DataConnectionFactory)"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="680" y="520" width="300" height="70" as="geometry" />
</mxCell>
<mxCell id="sr_dc" value="DataConnections.OpcUa/&#10;OpcUaDataConnection.cs&#10;(consumes typed model)"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="680" y="600" width="300" height="70" as="geometry" />
</mxCell>
<!-- referenced-by edges: both sides reference Commons -->
<mxCell id="ref_cui" value="referenced by"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;startArrow=none;fontSize=12;dashed=0;"
edge="1" parent="1" source="commons" target="centralui">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="280" y="400" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="ref_sr" value="referenced by"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;startArrow=none;fontSize=12;dashed=0;"
edge="1" parent="1" source="commons" target="siteruntime">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="830" y="400" />
</Array>
</mxGeometry>
</mxCell>
<!-- caption note -->
<mxCell id="note" value="Both sides deserialize DataConnection.PrimaryConfiguration / BackupConfiguration&#10;into the same OpcUaEndpointConfig instance. The DB column type does not change."
style="text;html=1;align=center;verticalAlign=middle;fontSize=12;fontStyle=2;fontColor=#555555;"
vertex="1" parent="1">
<mxGeometry x="200" y="730" width="720" height="50" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

@@ -0,0 +1,93 @@
<mxfile host="app.diagrams.net">
<diagram id="browserarch" name="Tag Browser Architecture">
<mxGraphModel dx="1000" dy="1200" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1300" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- 1. Blazor Server browser -->
<mxCell id="n1" value="Blazor Server browser"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="240" y="40" width="320" height="50" as="geometry" />
</mxCell>
<!-- 2. CentralUI: InstanceConfigure.razor -->
<mxCell id="n2" value="CentralUI: InstanceConfigure.razor"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="240" y="150" width="320" height="50" as="geometry" />
</mxCell>
<!-- 3. CentralUI: <OpcUaBrowserDialog/> -->
<mxCell id="n3" value="CentralUI: &amp;lt;OpcUaBrowserDialog/&amp;gt;"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="240" y="260" width="320" height="50" as="geometry" />
</mxCell>
<!-- 4. CentralUI: IOpcUaBrowseService -->
<mxCell id="n4" value="CentralUI: IOpcUaBrowseService"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="240" y="370" width="320" height="50" as="geometry" />
</mxCell>
<!-- 5. CommunicationService.SendCommandToSiteAsync -->
<mxCell id="n5" value="CommunicationService.SendCommandToSiteAsync&lt;BrowseOpcUaNodeResult&gt;(siteId, BrowseOpcUaNodeCommand)"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="180" y="480" width="440" height="60" as="geometry" />
</mxCell>
<!-- 6. Site: CentralCommunicationActor -> DataConnectionManagerActor -->
<mxCell id="n6" value="Site: CentralCommunicationActor &#8594; DataConnectionManagerActor"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="200" y="600" width="400" height="50" as="geometry" />
</mxCell>
<!-- 7. OPC UA server -->
<mxCell id="n7" value="OPC UA server"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=13;fontStyle=1;"
vertex="1" parent="1">
<mxGeometry x="280" y="710" width="240" height="50" as="geometry" />
</mxCell>
<!-- Edges with labels -->
<mxCell id="e1" value="SignalR"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;"
edge="1" parent="1" source="n1" target="n2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" value="opens"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;"
edge="1" parent="1" source="n2" target="n3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" value="uses"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;"
edge="1" parent="1" source="n3" target="n4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" value="implementation calls"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;"
edge="1" parent="1" source="n4" target="n5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" value="ClusterClient Ask&#10;ManagementEnvelope { User, Command, CorrelationId }"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="n5" target="n6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" value="dispatches to IBrowsableDataConnection (RealOpcUaClient)&#10;OPC Foundation .NET SDK Browse service"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="n6" target="n7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

@@ -0,0 +1,155 @@
<mxfile host="app.diagrams.net">
<diagram id="cmdflow" name="Browse Command Flow">
<mxGraphModel dx="1200" dy="1400" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="1500" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Step 1: CentralUI.OpcUaBrowseService -->
<mxCell id="s1" value="CentralUI.OpcUaBrowseService.BrowseChildrenAsync(siteId, connId, parent)"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="120" y="40" width="440" height="50" as="geometry" />
</mxCell>
<!-- Step 2: CommunicationService.SendCommandToSiteAsync -->
<mxCell id="s2" value="CommunicationService.SendCommandToSiteAsync&lt;BrowseOpcUaNodeResult&gt;(&#10; siteId, new BrowseOpcUaNodeCommand(connId, parent))"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="120" y="150" width="440" height="60" as="geometry" />
</mxCell>
<!-- Step 3: ManagementEnvelope over ClusterClient -->
<mxCell id="s3" value="ManagementEnvelope { User, Command, CorrelationId }&#10;over ClusterClient"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="160" y="270" width="360" height="60" as="geometry" />
</mxCell>
<!-- Step 4: Site: CentralCommunicationActor unwraps envelope -->
<mxCell id="s4" value="Site: CentralCommunicationActor unwraps envelope"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="160" y="390" width="360" height="50" as="geometry" />
</mxCell>
<!-- Step 5: Site: DataConnectionManagerActor receives command -->
<mxCell id="s5" value="Site: DataConnectionManagerActor receives BrowseOpcUaNodeCommand&#10;(DCL coordinator actor — owns the per-connection IDataConnection instances)"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="120" y="490" width="440" height="60" as="geometry" />
</mxCell>
<!-- Handler logic box (the bulleted sub-steps) -->
<mxCell id="handler" value="Handler logic"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;fontStyle=1;verticalAlign=top;spacingTop=6;align=center;"
vertex="1" parent="1">
<mxGeometry x="660" y="470" width="380" height="380" as="geometry" />
</mxCell>
<mxCell id="h_lookup" value="Look up IDataConnection by Id"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d6b656;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="500" width="340" height="30" as="geometry" />
</mxCell>
<mxCell id="h_notfound" value="if not found &#8594; ConnectionNotFound"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="540" width="340" height="30" as="geometry" />
</mxCell>
<mxCell id="h_notbrowsable" value="if !(conn is IBrowsableDataConnection) &#8594; NotBrowsable"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="580" width="340" height="30" as="geometry" />
</mxCell>
<mxCell id="h_await" value="else await conn.BrowseChildrenAsync(ParentNodeId, ct)"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d6b656;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="620" width="340" height="30" as="geometry" />
</mxCell>
<mxCell id="h_notconn" value="Catch ConnectionNotConnectedException &#8594; ConnectionNotConnected"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="660" width="340" height="30" as="geometry" />
</mxCell>
<mxCell id="h_cancel" value="Catch OperationCanceledException &#8594; Timeout"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="700" width="340" height="30" as="geometry" />
</mxCell>
<mxCell id="h_svc" value="Catch ServiceResultException &#8594; ServerError + verbatim msg"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="740" width="340" height="30" as="geometry" />
</mxCell>
<mxCell id="h_success" value="Else success: BrowseOpcUaNodeResult(children, truncated, null)"
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;align=left;spacingLeft=8;"
vertex="1" parent="1">
<mxGeometry x="680" y="790" width="340" height="40" as="geometry" />
</mxCell>
<!-- auth note -->
<mxCell id="authnote" value="Auth: site-side handler validates ManagementEnvelope.User has the Design role;&#10;otherwise reply ServerError (&quot;Not authorized to browse data connections&quot;)."
style="text;html=1;align=left;verticalAlign=middle;fontSize=11;fontStyle=2;fontColor=#555555;"
vertex="1" parent="1">
<mxGeometry x="120" y="570" width="430" height="40" as="geometry" />
</mxCell>
<!-- Reply path -->
<mxCell id="r1" value="Reply travels back via&#10;CentralCommunicationActor &#8594; CommunicationService"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="120" y="900" width="440" height="60" as="geometry" />
</mxCell>
<mxCell id="r2" value="returned to CentralUI page"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="200" y="1010" width="280" height="50" as="geometry" />
</mxCell>
<!-- Main chain edges -->
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;"
edge="1" parent="1" source="s1" target="s2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;"
edge="1" parent="1" source="s2" target="s3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;"
edge="1" parent="1" source="s3" target="s4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;"
edge="1" parent="1" source="s4" target="s5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- s5 -> handler logic -->
<mxCell id="e5" value="processes"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="s5" target="handler">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- handler -> reply -->
<mxCell id="e6" value="result / failure"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="handler" target="r1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="850" y="930" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;"
edge="1" parent="1" source="r1" target="r2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

@@ -0,0 +1,166 @@
<mxfile host="app.diagrams.net">
<diagram id="failover" name="Failover State Machine">
<mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="1300" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Connected (start state) -->
<mxCell id="connected" value="Connected"
style="rounded=1;arcSize=40;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;fontStyle=1;"
vertex="1" parent="1">
<mxGeometry x="420" y="40" width="200" height="50" as="geometry" />
</mxCell>
<!-- Push bad quality -->
<mxCell id="badquality" value="Push bad quality&#10;to all subscribers"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="420" y="150" width="200" height="50" as="geometry" />
</mxCell>
<!-- Retry active endpoint -->
<mxCell id="retry" value="Retry active endpoint&#10;(5s interval)"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="420" y="260" width="200" height="50" as="geometry" />
</mxCell>
<!-- _consecutiveFailures++ -->
<mxCell id="incr" value="_consecutiveFailures++"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="420" y="370" width="200" height="50" as="geometry" />
</mxCell>
<!-- Branch decision diamond -->
<mxCell id="branch" value="Evaluate&#10;_consecutiveFailures"
style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;"
vertex="1" parent="1">
<mxGeometry x="450" y="470" width="140" height="100" as="geometry" />
</mxCell>
<!-- Branch 1: retry same endpoint -->
<mxCell id="same" value="Retry same endpoint"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="80" y="495" width="200" height="50" as="geometry" />
</mxCell>
<!-- Branch 2: failover to backup -->
<mxCell id="failover" value="Failover&#10;&#8226; dispose adapter, switch _activeEndpoint, reset counter&#10;&#8226; create fresh adapter with other config&#10;&#8226; attempt connect"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;align=left;spacingLeft=8;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="380" y="640" width="280" height="110" as="geometry" />
</mxCell>
<!-- Branch 3: no backup -->
<mxCell id="nobackup" value="Keep retrying indefinitely&#10;(current behavior)"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=13;"
vertex="1" parent="1">
<mxGeometry x="760" y="495" width="220" height="60" as="geometry" />
</mxCell>
<!-- Successful reconnect outcome -->
<mxCell id="reconnect" value="On successful reconnect (either endpoint)&#10;1. Reset _consecutiveFailures = 0&#10;2. ReSubscribeAll() — re-create subscriptions on new adapter&#10;3. Transition to Connected&#10;4. Log failover event if endpoint changed&#10;5. Report active endpoint in health metrics"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;align=left;spacingLeft=10;verticalAlign=middle;"
vertex="1" parent="1">
<mxGeometry x="360" y="830" width="320" height="140" as="geometry" />
</mxCell>
<!-- Edges -->
<mxCell id="e1" value="disconnect detected"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;"
edge="1" parent="1" source="connected" target="badquality">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;"
edge="1" parent="1" source="badquality" target="retry">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" value="failure"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;"
edge="1" parent="1" source="retry" target="incr">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;"
edge="1" parent="1" source="incr" target="branch">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Branch 1 edge -->
<mxCell id="b1" value="&lt; FailoverRetryCount"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="branch" target="same">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Branch 1 loops back to retry -->
<mxCell id="b1loop" value="retry"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;dashed=1;"
edge="1" parent="1" source="same" target="retry">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="180" y="285" />
</Array>
</mxGeometry>
</mxCell>
<!-- Branch 2 edge -->
<mxCell id="b2" value="&#8805; FailoverRetryCount&#10;AND backup exists"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="branch" target="failover">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Branch 3 edge -->
<mxCell id="b3" value="&#8805; FailoverRetryCount&#10;AND no backup"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="branch" target="nobackup">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- no backup loops back to retry -->
<mxCell id="b3loop" value="retry (round-robin n/a)"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;dashed=1;"
edge="1" parent="1" source="nobackup" target="retry">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="870" y="285" />
</Array>
</mxGeometry>
</mxCell>
<!-- failover -> attempt connect -> success -> reconnect -->
<mxCell id="b2ok" value="connect succeeds"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="failover" target="reconnect">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- failover -> attempt connect -> failure -> back to retry loop -->
<mxCell id="b2fail" value="connect fails&#10;(round-robin: primary &#8594; backup &#8594; primary...)"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;dashed=1;"
edge="1" parent="1" source="failover" target="retry">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="700" y="695" />
<mxPoint x="700" y="285" />
</Array>
</mxGeometry>
</mxCell>
<!-- reconnect -> Connected -->
<mxCell id="bdone" value="Transition to Connected"
style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;"
edge="1" parent="1" source="reconnect" target="connected">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="320" y="900" />
<mxPoint x="320" y="65" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

+2 -21
View File
@@ -547,27 +547,8 @@ This section governs how implementation plans are executed. The goal is autonomo
For each work package, follow this sequence: For each work package, follow this sequence:
``` ![generate-plans-per-work-package](diagrams/generate-plans-per-work-package.png)
┌─────────────────────────────────────────────────────┐ <!-- source: diagrams/generate-plans-per-work-package.drawio — edit, then re-export with export-drawio.sh -->
│ 1. READ the WP description and acceptance criteria │
│ 2. READ all traced requirements (HLR bullets, KDD, │
│ CD constraints) to understand intent │
│ 3. IMPLEMENT the WP │
│ - Write code │
│ - Write unit tests for acceptance criteria │
│ - Write negative tests for prohibition criteria │
│ 4. VERIFY acceptance criteria │
│ - Run tests: all must pass │
│ - Walk each acceptance criterion line by line │
│ - If a criterion cannot be verified yet (depends │
│ on a later WP), note it as "deferred to WP-N" │
│ 5. UPDATE the phase execution checklist │
│ - Mark WP as complete with date │
│ - Note any deferred criteria │
│ - Note any questions logged │
│ 6. COMMIT with message: "Phase N WP-M: <summary>" │
└─────────────────────────────────────────────────────┘
```
### Mid-Phase Compliance Check ### Mid-Phase Compliance Check
+6 -60
View File
@@ -23,29 +23,8 @@ gRPC server-streaming is an established pattern for real-time tag value updates;
## Architecture ## Architecture
``` ![grpc-streams-architecture](diagrams/grpc-streams-architecture.png)
Central Cluster Site Cluster <!-- source: diagrams/grpc-streams-architecture.drawio — edit, then re-export with export-drawio.sh -->
───────────── ────────────
DebugStreamBridgeActor InstanceActor
│ │
│── SubscribeDebugView ──► │ (ClusterClient: command/control)
│◄── DebugViewSnapshot ── │
│ │
│ │ publishes AttributeValueChanged
│ │ publishes AlarmStateChanged
│ ▼
SiteStreamGrpcClient ◄──── gRPC stream ───── SiteStreamGrpcServer
(per-site, on central) (HTTP/2) (Kestrel, on site)
│ │
│ reads from gRPC stream │ receives from SiteStreamManager
│ routes by correlationId │ filters by instance name
▼ │
DebugStreamBridgeActor │
│ │
▼ │
SignalR Hub / Blazor UI │
```
**Key separation**: ClusterClient handles subscribe/unsubscribe/snapshot (request-response). gRPC handles the ongoing value stream (server-streaming). **Key separation**: ClusterClient handles subscribe/unsubscribe/snapshot (request-response). gRPC handles the ongoing value stream (server-streaming).
@@ -271,17 +250,8 @@ public override async Task SubscribeInstance(
`IServerStreamWriter<T>` is **not thread-safe**. Multiple Akka actors may publish events concurrently. The `Channel<SiteStreamEvent>` bridges these worlds: `IServerStreamWriter<T>` is **not thread-safe**. Multiple Akka actors may publish events concurrently. The `Channel<SiteStreamEvent>` bridges these worlds:
``` ![grpc-channel-bridging](diagrams/grpc-channel-bridging.png)
Akka Actor Thread(s) gRPC Response Stream <!-- source: diagrams/grpc-channel-bridging.drawio — edit, then re-export with export-drawio.sh -->
│ ▲
│ channel.Writer.TryWrite(evt) │ await responseStream.WriteAsync(evt)
▼ │
┌─────────────────────────────────────────┐
│ Channel<SiteStreamEvent> │
│ BoundedChannelOptions(1000) │
│ FullMode = DropOldest │
└─────────────────────────────────────────┘
```
- **Bounded capacity** (1000): prevents unbounded memory growth if the gRPC client is slow - **Bounded capacity** (1000): prevents unbounded memory growth if the gRPC client is slow
- **DropOldest**: matches the existing `SiteStreamManager` overflow strategy - **DropOldest**: matches the existing `SiteStreamManager` overflow strategy
@@ -431,32 +401,8 @@ private void HandleGrpcStreamError(Exception ex)
### Reconnection State Machine (DebugStreamBridgeActor) ### Reconnection State Machine (DebugStreamBridgeActor)
``` ![grpc-reconnection-state-machine](diagrams/grpc-reconnection-state-machine.png)
┌──────────────────┐ <!-- source: diagrams/grpc-reconnection-state-machine.drawio — edit, then re-export with export-drawio.sh -->
│ Streaming │ ◄── Normal state: gRPC stream active
└────────┬─────────┘
│ gRPC stream error / keepalive timeout
┌──────────────────┐
┌──► │ Reconnecting │ ── try other node endpoint
│ └────────┬─────────┘
│ │
│ ┌────────┴─────────┐
│ │ │
│ success failure (retry < max)
│ │ │
│ ▼ │
│ Streaming schedule retry (5s backoff)
│ │
└───────────────────────┘
failure (retry >= max)
┌──────────────────┐
│ Terminated │ ── notify consumer, stop actor
└──────────────────┘
```
### Summary ### Summary
+2 -14
View File
@@ -167,20 +167,8 @@ Keepalive settings are configurable via `CommunicationOptions`:
## Topology ## Topology
``` ![communication-topology](diagrams/communication-topology.png)
Central Cluster <!-- source: diagrams/communication-topology.drawio — edit, then re-export with export-drawio.sh -->
├── ClusterClient → Site A Cluster (SiteCommunicationActor via Receptionist) [command/control]
├── ClusterClient → Site B Cluster (SiteCommunicationActor via Receptionist) [command/control]
└── ClusterClient → Site N Cluster (SiteCommunicationActor via Receptionist) [command/control]
├── SiteStreamGrpcClient ◄── gRPC stream ── Site A (SiteStreamGrpcServer) [real-time data]
├── SiteStreamGrpcClient ◄── gRPC stream ── Site B (SiteStreamGrpcServer) [real-time data]
└── SiteStreamGrpcClient ◄── gRPC stream ── Site N (SiteStreamGrpcServer) [real-time data]
Site Clusters
└── ClusterClient → Central Cluster (CentralCommunicationActor via Receptionist) [command/control]
└── SiteStreamGrpcServer (Kestrel HTTP/2, port 8083) → serves gRPC streams [real-time data]
```
- Sites do **not** communicate with each other. - Sites do **not** communicate with each other.
- All inter-cluster communication flows through central. - All inter-cluster communication flows through central.
@@ -143,15 +143,8 @@ EF Core's DbContext naturally provides unit-of-work semantics:
### Example Transactional Flow ### Example Transactional Flow
``` ![configdb-transactional-flow](diagrams/configdb-transactional-flow.png)
Template Engine: Create Template <!-- source: diagrams/configdb-transactional-flow.drawio — edit, then re-export with export-drawio.sh -->
├── repository.AddTemplate(template) // template is a Commons POCO
├── repository.AddAttributes(attributes) // attributes are Commons POCOs
├── repository.AddAlarms(alarms) // alarms are Commons POCOs
├── repository.AddScripts(scripts) // scripts are Commons POCOs
└── repository.SaveChangesAsync() // single transaction commits all
```
--- ---
@@ -184,14 +177,8 @@ Audit entries are written **synchronously** within the same database transaction
### Integration Example ### Integration Example
``` ![configdb-integration-example](diagrams/configdb-integration-example.png)
Template Engine: Update Template <!-- source: diagrams/configdb-integration-example.drawio — edit, then re-export with export-drawio.sh -->
├── repository.UpdateTemplate(template)
├── auditService.LogAsync(user, "Update", "Template", template.Id,
│ template.Name, template)
└── repository.SaveChangesAsync() ← both the change and audit entry commit together
```
### Audit Entry Schema ### Audit Entry Schema
@@ -80,12 +80,8 @@ Data connections support an optional backup endpoint for automatic failover when
**Failover state machine:** **Failover state machine:**
``` ![dcl-endpoint-redundancy](diagrams/dcl-endpoint-redundancy.png)
Connected → disconnect → push bad quality → retry active endpoint (5s) <!-- source: diagrams/dcl-endpoint-redundancy.drawio — edit, then re-export with export-drawio.sh -->
→ N failures (≥ FailoverRetryCount) → switch to other endpoint
→ dispose adapter, create fresh adapter with other config
→ reconnect → ReSubscribeAll → Connected
```
- **Round-robin**: primary → backup → primary → backup. No preferred endpoint after first failover — the connection stays on whichever endpoint is working. - **Round-robin**: primary → backup → primary → backup. No preferred endpoint after first failover — the connection stays on whichever endpoint is working.
- **No auto-failback**: The connection remains on the active endpoint until it fails. - **No auto-failback**: The connection remains on the active endpoint until it fails.
@@ -22,24 +22,8 @@ Central cluster only. The site-side deployment responsibilities (receiving confi
## Deployment Flow ## Deployment Flow
``` ![deployment-flow](diagrams/deployment-flow.png)
Engineer (UI) → Deployment Manager (Central) <!-- source: diagrams/deployment-flow.drawio — edit, then re-export with export-drawio.sh -->
├── 1. Request validated + flattened config from Template Engine
│ (validation includes flattening, script compilation,
│ trigger references, connection binding completeness)
├── 2. If validation fails → return errors to UI, stop
├── 3. Send config to site via Communication Layer
│ │
│ ▼
│ Site Runtime (Deployment Manager Singleton)
│ ├── 4. Store new flattened config locally (SQLite)
│ ├── 5. Compile scripts at site
│ ├── 6. Create/update Instance Actor (with child Script + Alarm Actors)
│ └── 7. Report success/failure back to central
└── 8. Update deployment status in config DB
```
## Deployment Identity & Idempotency ## Deployment Identity & Idempotency
+2 -14
View File
@@ -123,20 +123,8 @@ API method scripts are compiled at central startup — all method definitions ar
## Request Flow ## Request Flow
``` ![inboundapi-request-flow](diagrams/inboundapi-request-flow.png)
External System <!-- source: diagrams/inboundapi-request-flow.drawio — edit, then re-export with export-drawio.sh -->
Inbound API (Central)
├── 1. Extract API key from request
├── 2. Validate key exists and is enabled
├── 3. Resolve method by name
├── 4. Check API key is in method's approved list
├── 5. Validate and deserialize parameters
├── 6. Execute implementation script (subject to method timeout)
├── 7. Serialize return value
└── 8. Return response
```
## Implementation Script Capabilities ## Implementation Script Capabilities
@@ -24,24 +24,8 @@ SMTP and HTTP delivery is blocking I/O. Delivery work runs on a **dedicated bloc
## End-to-End Flow ## End-to-End Flow
``` ![notificationoutbox-flow](diagrams/notificationoutbox-flow.png)
Site script: Notify.To("list").Send(subject, body) <!-- source: diagrams/notificationoutbox-flow.drawio — edit, then re-export with export-drawio.sh -->
│ generate NotificationId (GUID) locally; return it to the script immediately
Site Store-and-Forward Engine (notification category, target = central)
│ durably forwards to central via CentralSite Communication (ClusterClient);
│ buffers/retries if central is unreachable
Central ingest: insert-if-not-exists on NotificationId → Notifications table (Pending)
│ ack the site → site S&F clears the message
Central Notification Outbox actor (singleton, active central node)
│ polls due rows; resolves the list; delivers via the matching adapter
├── success → Delivered
├── transient failure → Retrying (schedule NextAttemptAt)
└── permanent failure
/ retries exhausted → Parked
```
The site forwards only `(listName, subject, body)` plus provenance — recipient resolution happens at central, at delivery time. This keeps notification-list definitions in one place and removes the deploy-to-sites artifact entirely. The site forwards only `(listName, subject, body)` plus provenance — recipient resolution happens at central, at delivery time. This keeps notification-list definitions in one place and removes the deploy-to-sites artifact entirely.
+2 -15
View File
@@ -27,21 +27,8 @@ Site clusters only.
## Actor Hierarchy ## Actor Hierarchy
``` ![siteruntime-actor-hierarchy](diagrams/siteruntime-actor-hierarchy.png)
Deployment Manager Singleton (Cluster Singleton) <!-- source: diagrams/siteruntime-actor-hierarchy.drawio — edit, then re-export with export-drawio.sh -->
├── Instance Actor ("MachineA-001")
│ ├── Script Actor ("MonitorSpeed") — coordinator
│ │ └── Script Execution Actor — short-lived, per invocation
│ ├── Script Actor ("CalculateOEE") — coordinator
│ │ └── Script Execution Actor — short-lived, per invocation
│ ├── Alarm Actor ("OverTemp") — coordinator (computed)
│ │ └── Alarm Execution Actor — short-lived, per on-trigger invocation
│ ├── Alarm Actor ("LowPressure") — coordinator (computed)
│ └── Native Alarm Actor ("OpcUaServer1") — read-only mirror, peer to Alarm Actor
├── Instance Actor ("MachineA-002")
│ └── ...
└── ...
```
--- ---
+2 -17
View File
@@ -25,23 +25,8 @@ Site clusters only. The central cluster does not buffer messages.
## Message Lifecycle ## Message Lifecycle
``` ![storeforward-message-lifecycle](diagrams/storeforward-message-lifecycle.png)
Script submits message <!-- source: diagrams/storeforward-message-lifecycle.drawio — edit, then re-export with export-drawio.sh -->
Attempt immediate delivery
├── Success → Remove from buffer
└── Failure → Buffer message
Retry loop (per retry policy)
├── Success → Remove from buffer + notify standby
└── Max retries exhausted → Park message
```
For notifications, "delivery" means forwarding the message to the central cluster via CentralSite Communication; "success" is central's ack, on which the message is cleared. Notifications are retried at the fixed forward interval until central acks, but — like every other category — they are bounded by the engine's `DefaultMaxRetries` cap: a sustained central outage that exceeds `DefaultMaxRetries × forward-interval` will park the buffered notification, after which an operator can Retry/Discard it via the parked-message UI. Operationally, the cap is sized so the normal central-recovery window stays well inside it; "do not park" is the design's operational intent on the happy path, not an absolute invariant. Callers that genuinely require unbounded retry pass `maxRetries: 0` on `EnqueueAsync` (the documented "no limit" escape hatch — see `StoreAndForward-015`). For notifications, "delivery" means forwarding the message to the central cluster via CentralSite Communication; "success" is central's ack, on which the message is cleared. Notifications are retried at the fixed forward interval until central acks, but — like every other category — they are bounded by the engine's `DefaultMaxRetries` cap: a sustained central outage that exceeds `DefaultMaxRetries × forward-interval` will park the buffered notification, after which an operator can Retry/Discard it via the parked-message UI. Operationally, the cap is sized so the normal central-recovery window stays well inside it; "do not park" is the design's operational intent on the happy path, not an absolute invariant. Callers that genuinely require unbounded retry pass `maxRetries: 0` on `EnqueueAsync` (the documented "no limit" escape hatch — see `StoreAndForward-015`).
+6 -59
View File
@@ -92,20 +92,8 @@ The manifest is plaintext so the import wizard can preview bundle contents and s
## Architecture ## Architecture
``` ![transport-architecture](diagrams/transport-architecture.png)
ZB.MOM.WW.ScadaBridge.Transport <!-- source: diagrams/transport-architecture.drawio — edit, then re-export with export-drawio.sh -->
├── IBundleExporter
│ ExportAsync(ExportSelection, Passphrase?, ct) → Stream
├── IBundleImporter
│ LoadAsync(stream, Passphrase?, ct) → BundleSession
│ PreviewAsync(sessionId, ct) → ImportPreview
│ ApplyAsync(sessionId, resolutions, ct) → ImportResult
├── DependencyResolver
├── BundleSerializer (manifest + content JSON; ZIP packer)
├── BundleSecretEncryptor (AES-256-GCM + PBKDF2)
├── BundleSessionStore (in-memory, TTL'd)
└── ManifestValidator (schema/version gating, hash check)
```
The component is central-only. It is registered in `ZB.MOM.WW.ScadaBridge.Host` for central roles only, never for site roles. All persistence flows through existing audited repository interfaces in `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase` — the component does not call `DbContext.SaveChangesAsync` directly. `BundleSessionStore` is in-process on the active central node (matching Blazor Server circuit affinity): 30-minute TTL, eviction on expiry, 3-strike passphrase lockout per session. The component is central-only. It is registered in `ZB.MOM.WW.ScadaBridge.Host` for central roles only, never for site roles. All persistence flows through existing audited repository interfaces in `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase` — the component does not call `DbContext.SaveChangesAsync` directly. `BundleSessionStore` is in-process on the active central node (matching Blazor Server circuit affinity): 30-minute TTL, eviction on expiry, 3-strike passphrase lockout per session.
@@ -132,22 +120,8 @@ The user can toggle "include all dependencies" off (with a warning that the bund
### Backend ### Backend
``` ![transport-export-flow](diagrams/transport-export-flow.png)
User (Design role) ─► Central UI Export wizard <!-- source: diagrams/transport-export-flow.drawio — edit, then re-export with export-drawio.sh -->
IBundleExporter
├─► DependencyResolver ─► repositories (read)
├─► EntitySerializer ─► content.json
├─► BundleSecretEncryptor ► content.enc (if passphrase)
├─► ManifestBuilder ─► manifest.json
ZIP packer → temp file → browser download
IAuditService.LogAsync(BundleExported …)
```
Audit event: `BundleExported` — caller, artifact count, content hash, encrypted yes/no, bundle filename. Audit event: `BundleExported` — caller, artifact count, content hash, encrypted yes/no, bundle filename.
@@ -179,35 +153,8 @@ Bundle references that cannot be satisfied in either the bundle or the target DB
### Backend ### Backend
``` ![transport-import-flow](diagrams/transport-import-flow.png)
User (Admin role) ─► uploads bundle <!-- source: diagrams/transport-import-flow.drawio — edit, then re-export with export-drawio.sh -->
IBundleImporter.LoadAsync
· verify SHA-256 (manifest vs content)
· check bundleFormatVersion supported
· decrypt content.enc with passphrase (if encrypted)
· deserialize entities
· open BundleSession (30-min TTL)
PreviewAsync → diff vs target DB → ImportPreview
▼ (user reviews + resolves conflicts)
ApplyAsync (single EF transaction)
· run two-tier semantic validation (minimal name scan + full SemanticValidator)
· apply resolutions (add / overwrite / skip / rename)
· upsert TemplateFolder hierarchy
· IAuditService.LogAsync(BundleImported …)
· commit
ImportResult → UI step 5
"View on Deployments →" (existing page)
```
Authorization: `RequireAdmin` on both the Razor page and `IBundleImporter.*` entrypoints. Authorization: `RequireAdmin` on both the Razor page and `IBundleImporter.*` entrypoints.
@@ -0,0 +1,116 @@
<mxfile host="app.diagrams.net">
<diagram id="topology" name="Topology">
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="1300" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Central Cluster container -->
<mxCell id="central" value="Central Cluster" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;verticalAlign=top;fontStyle=1;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="60" y="60" width="380" height="320" as="geometry" />
</mxCell>
<mxCell id="cc-a" value="ClusterClient&#10;(command/control)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="90" y="110" width="150" height="50" as="geometry" />
</mxCell>
<mxCell id="cc-b" value="ClusterClient&#10;(command/control)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="90" y="170" width="150" height="50" as="geometry" />
</mxCell>
<mxCell id="cc-n" value="ClusterClient&#10;(command/control)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="90" y="230" width="150" height="50" as="geometry" />
</mxCell>
<mxCell id="grpc-client" value="SiteStreamGrpcClient&#10;(real-time data)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="90" y="300" width="150" height="50" as="geometry" />
</mxCell>
<!-- Site A -->
<mxCell id="siteA" value="Site A Cluster" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;verticalAlign=top;fontStyle=1;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="720" y="60" width="380" height="250" as="geometry" />
</mxCell>
<mxCell id="siteA-comm" value="SiteCommunicationActor&#10;(via Receptionist)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="760" y="110" width="190" height="50" as="geometry" />
</mxCell>
<mxCell id="siteA-grpc" value="SiteStreamGrpcServer&#10;(Kestrel HTTP/2, port 8083)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="760" y="180" width="190" height="50" as="geometry" />
</mxCell>
<mxCell id="siteA-clusterclient" value="ClusterClient &#8594; Central&#10;(CentralCommunicationActor)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="760" y="245" width="190" height="50" as="geometry" />
</mxCell>
<!-- Site B -->
<mxCell id="siteB" value="Site B Cluster" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;verticalAlign=top;fontStyle=1;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="720" y="370" width="380" height="130" as="geometry" />
</mxCell>
<mxCell id="siteB-comm" value="SiteCommunicationActor&#10;(via Receptionist)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="760" y="410" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="siteB-grpc" value="SiteStreamGrpcServer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="760" y="455" width="190" height="35" as="geometry" />
</mxCell>
<!-- Site N -->
<mxCell id="siteN" value="Site N Cluster" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;verticalAlign=top;fontStyle=1;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="720" y="560" width="380" height="130" as="geometry" />
</mxCell>
<mxCell id="siteN-comm" value="SiteCommunicationActor&#10;(via Receptionist)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="760" y="600" width="190" height="40" as="geometry" />
</mxCell>
<mxCell id="siteN-grpc" value="SiteStreamGrpcServer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="760" y="645" width="190" height="35" as="geometry" />
</mxCell>
<!-- Command/control edges: Central ClusterClients -> Site comm actors -->
<mxCell id="e-cca" value="command/control" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;" edge="1" parent="1" source="cc-a" target="siteA-comm">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-ccb" value="command/control" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;" edge="1" parent="1" source="cc-b" target="siteB-comm">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-ccn" value="command/control" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;" edge="1" parent="1" source="cc-n" target="siteN-comm">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- gRPC stream edges: site servers -> central grpc client (data flows site->central) -->
<mxCell id="e-grpca" value="gRPC stream (real-time data)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="siteA-grpc" target="grpc-client">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="540" y="205" />
<mxPoint x="540" y="325" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="e-grpcb" value="gRPC stream" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="siteB-grpc" target="grpc-client">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="600" y="472" />
<mxPoint x="600" y="335" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="e-grpcn" value="gRPC stream" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="siteN-grpc" target="grpc-client">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="660" y="662" />
<mxPoint x="660" y="345" />
</Array>
</mxGeometry>
</mxCell>
<!-- Site A -> Central command/control (ClusterClient to central) -->
<mxCell id="e-sa-central" value="command/control" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;dashed=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.7;entryDx=0;entryDy=0;" edge="1" parent="1" source="siteA-clusterclient" target="central">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="480" y="270" />
<mxPoint x="480" y="284" />
</Array>
</mxGeometry>
</mxCell>
<!-- Note: sites do not talk to each other -->
<mxCell id="note" value="Sites do NOT communicate with each other.&#10;All inter-cluster communication flows through Central." style="shape=note;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="60" y="560" width="380" height="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

@@ -0,0 +1,37 @@
<mxfile host="app.diagrams.net">
<diagram id="configdb-integration-example" name="Integration Example">
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="start" value="Template Engine: Update Template" style="rounded=1;arcSize=40;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="220" y="40" width="360" height="50" as="geometry" />
</mxCell>
<mxCell id="upd" value="repository.UpdateTemplate(template)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="220" y="150" width="360" height="50" as="geometry" />
</mxCell>
<mxCell id="audit" value="auditService.LogAsync(user, &quot;Update&quot;, &quot;Template&quot;,&#10;&#160;&#160;&#160;&#160;template.Id, template.Name, template)" style="whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="220" y="230" width="360" height="60" as="geometry" />
</mxCell>
<mxCell id="save" value="repository.SaveChangesAsync()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="220" y="330" width="360" height="50" as="geometry" />
</mxCell>
<mxCell id="note" value="both the change and audit entry&#10;commit together" style="shape=note;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;size=14;align=left;spacingLeft=10;" vertex="1" parent="1">
<mxGeometry x="630" y="330" width="180" height="50" as="geometry" />
</mxCell>
<mxCell id="e0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="start" target="upd">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="upd" target="audit">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="audit" target="save">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=none;dashed=1;" edge="1" parent="1" source="save" target="note">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

@@ -0,0 +1,49 @@
<mxfile host="app.diagrams.net">
<diagram id="configdb-transactional-flow" name="Transactional Flow">
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="start" value="Template Engine: Create Template" style="rounded=1;arcSize=40;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="240" y="40" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="add1" value="repository.AddTemplate(template)&#10;// template is a Commons POCO" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="240" y="150" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="add2" value="repository.AddAttributes(attributes)&#10;// attributes are Commons POCOs" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="240" y="230" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="add3" value="repository.AddAlarms(alarms)&#10;// alarms are Commons POCOs" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="240" y="310" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="add4" value="repository.AddScripts(scripts)&#10;// scripts are Commons POCOs" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="240" y="390" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="save" value="repository.SaveChangesAsync()&#10;// single transaction commits all" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="240" y="480" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="db" value="Configuration DB&#10;(MS SQL)" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="640" y="475" width="120" height="70" as="geometry" />
</mxCell>
<mxCell id="e0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="start" target="add1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="add1" target="add2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="add2" target="add3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="add3" target="add4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="add4" target="save">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" value="single transaction" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;dashed=1;" edge="1" parent="1" source="save" target="db">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

@@ -0,0 +1,71 @@
<mxfile host="app.diagrams.net">
<diagram id="dcl-endpoint-redundancy" name="Failover State Machine">
<mxGraphModel dx="900" dy="700" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="connected" value="Connected" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="360" y="40" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="pushbad" value="push bad quality" style="whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="360" y="150" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="retry" value="retry active endpoint&#10;(5s)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="360" y="250" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="decide" value="N failures&#10;(&#8805; FailoverRetryCount)?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="350" y="350" width="180" height="100" as="geometry" />
</mxCell>
<mxCell id="switch" value="switch to other endpoint" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="360" y="500" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="dispose" value="dispose adapter,&#10;create fresh adapter&#10;with other config" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="360" y="600" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="reconnect" value="reconnect" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="360" y="710" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="resub" value="ReSubscribeAll" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="360" y="800" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="e0" value="disconnect" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="connected" target="pushbad">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="pushbad" target="retry">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="retry" target="decide">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2b" value="no (retry again)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="decide" target="retry">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="620" y="400" />
<mxPoint x="620" y="275" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="e3" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="decide" target="switch">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="switch" target="dispose">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="dispose" target="reconnect">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="reconnect" target="resub">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e7" value="back to Connected" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="resub" target="connected">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="240" y="825" />
<mxPoint x="240" y="65" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@@ -0,0 +1,96 @@
<mxfile host="app.diagrams.net">
<diagram id="deployflow" name="Deployment Flow">
<mxGraphModel dx="1200" dy="1400" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="1500" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Start: Engineer -->
<mxCell id="engineer" value="Engineer (UI)" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="320" y="40" width="200" height="50" as="geometry" />
</mxCell>
<!-- Deployment Manager (Central) container header -->
<mxCell id="dm-central" value="Deployment Manager (Central)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;verticalAlign=top;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="120" y="130" width="640" height="430" as="geometry" />
</mxCell>
<mxCell id="step1" value="1. Request validated + flattened config from Template Engine&#10;(validation: flattening, script compilation, trigger references,&#10;connection binding completeness)" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="160" y="180" width="560" height="60" as="geometry" />
</mxCell>
<mxCell id="step2" value="2. Validation fails?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="350" y="270" width="180" height="80" as="geometry" />
</mxCell>
<mxCell id="step2fail" value="Return errors to UI, stop" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="600" y="280" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="step3" value="3. Send config to site via Communication Layer" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="220" y="390" width="440" height="50" as="geometry" />
</mxCell>
<mxCell id="step8" value="8. Update deployment status in config DB" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="280" y="480" width="320" height="70" as="geometry" />
</mxCell>
<!-- Site Runtime container -->
<mxCell id="site-runtime" value="Site Runtime (Deployment Manager Singleton)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;verticalAlign=top;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="120" y="620" width="640" height="380" as="geometry" />
</mxCell>
<mxCell id="step4" value="4. Store new flattened config locally (SQLite)" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="200" y="670" width="480" height="70" as="geometry" />
</mxCell>
<mxCell id="step5" value="5. Compile scripts at site" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="240" y="770" width="400" height="50" as="geometry" />
</mxCell>
<mxCell id="step6" value="6. Create/update Instance Actor&#10;(with child Script + Alarm Actors)" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="240" y="850" width="400" height="60" as="geometry" />
</mxCell>
<mxCell id="step7" value="7. Report success/failure back to central" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="240" y="940" width="400" height="50" as="geometry" />
</mxCell>
<!-- Edges -->
<mxCell id="e0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="engineer" target="step1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2yes" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="step2" target="step2fail">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2no" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="step2" target="step3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" value="config" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="step3" target="step4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="step4" target="step5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="step5" target="step6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="step6" target="step7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e7" value="report success/failure" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="step7" target="step8">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="780" y="965" />
<mxPoint x="780" y="515" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

@@ -0,0 +1,67 @@
<mxfile host="app.diagrams.net">
<diagram id="inboundapi-request-flow" name="Request Flow">
<mxGraphModel dx="900" dy="900" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="ext" value="External System" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="320" y="40" width="200" height="50" as="geometry" />
</mxCell>
<mxCell id="api" value="Inbound API (Central)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="320" y="140" width="200" height="50" as="geometry" />
</mxCell>
<mxCell id="s1" value="1. Extract API key from request" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="280" y="240" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="s2" value="2. Validate key exists and is enabled" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="280" y="300" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="s3" value="3. Resolve method by name" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="280" y="360" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="s4" value="4. Check API key is in method's approved list" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="280" y="420" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="s5" value="5. Validate and deserialize parameters" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="280" y="480" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="s6" value="6. Execute implementation script&#10;(subject to method timeout)" style="whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="280" y="540" width="280" height="50" as="geometry" />
</mxCell>
<mxCell id="s7" value="7. Serialize return value" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;" vertex="1" parent="1">
<mxGeometry x="280" y="610" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="s8" value="8. Return response" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="280" y="670" width="280" height="40" as="geometry" />
</mxCell>
<mxCell id="e0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="ext" target="api">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="api" target="s1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s1" target="s2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s2" target="s3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s3" target="s4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s4" target="s5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s5" target="s6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s6" target="s7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="s7" target="s8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

@@ -0,0 +1,83 @@
<mxfile host="app.diagrams.net">
<diagram id="outboxflow" name="End-to-End Flow">
<mxGraphModel dx="1000" dy="1400" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="950" pageHeight="1400" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Site script -->
<mxCell id="script" value="Site script: Notify.To(&quot;list&quot;).Send(subject, body)&#10;generate NotificationId (GUID) locally;&#10;return it to the script immediately" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="380" height="70" as="geometry" />
</mxCell>
<!-- Site S&F Engine -->
<mxCell id="snf" value="Site Store-and-Forward Engine&#10;(notification category, target = central)&#10;durably forwards to central via CentralSite Communication&#10;(ClusterClient); buffers/retries if central is unreachable" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="240" y="170" width="460" height="80" as="geometry" />
</mxCell>
<!-- Central ingest -->
<mxCell id="ingest" value="Central ingest: insert-if-not-exists on NotificationId&#10;→ Notifications table (Pending)&#10;ack the site → site S&amp;F clears the message" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="270" y="310" width="400" height="90" as="geometry" />
</mxCell>
<!-- Central Notification Outbox actor -->
<mxCell id="outbox" value="Central Notification Outbox actor&#10;(singleton, active central node)&#10;polls due rows; resolves the list;&#10;delivers via the matching adapter" style="whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="280" y="460" width="380" height="80" as="geometry" />
</mxCell>
<!-- Delivery decision -->
<mxCell id="d1" value="Delivery outcome" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="400" y="610" width="140" height="100" as="geometry" />
</mxCell>
<!-- Delivered -->
<mxCell id="delivered" value="Delivered" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="120" y="630" width="180" height="60" as="geometry" />
</mxCell>
<!-- Retrying -->
<mxCell id="retrying" value="Retrying&#10;(schedule NextAttemptAt)" style="rounded=1;arcSize=20;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="380" y="790" width="180" height="60" as="geometry" />
</mxCell>
<!-- Parked -->
<mxCell id="parked" value="Parked" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="650" y="630" width="180" height="60" as="geometry" />
</mxCell>
<!-- Edges -->
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="script" target="snf">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="snf" target="ingest">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="ingest" target="outbox">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="outbox" target="d1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" value="success" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="d1" target="delivered">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" value="transient failure" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="d1" target="retrying">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e7" value="permanent failure /&#10;retries exhausted" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="d1" target="parked">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Retrying loops back to outbox poll -->
<mxCell id="e8" value="retry due" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=11;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="retrying" target="outbox">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="780" y="820" />
<mxPoint x="780" y="500" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

@@ -0,0 +1,104 @@
<mxfile host="app.diagrams.net">
<diagram id="actorhierarchy" name="Actor Hierarchy">
<mxGraphModel dx="1400" dy="1000" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="1320" pageHeight="560" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Root: Deployment Manager Singleton -->
<mxCell id="dms" value="Deployment Manager Singleton&#10;(Cluster Singleton)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="490" y="30" width="280" height="60" as="geometry" />
</mxCell>
<!-- Instance Actors -->
<mxCell id="ia1" value="Instance Actor&#10;(&quot;MachineA-001&quot;)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="368" y="160" width="184" height="60" as="geometry" />
</mxCell>
<mxCell id="ia2" value="Instance Actor&#10;(&quot;MachineA-002&quot;)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="160" width="184" height="60" as="geometry" />
</mxCell>
<mxCell id="iamore" value="… more Instance Actors" style="text;html=1;align=center;verticalAlign=middle;fontSize=12;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="1190" y="170" width="120" height="40" as="geometry" />
</mxCell>
<!-- Children of MachineA-001 (top row) -->
<mxCell id="sa1" value="Script Actor (&quot;MonitorSpeed&quot;)&#10;— coordinator" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="16" y="300" width="164" height="74" as="geometry" />
</mxCell>
<mxCell id="sa2" value="Script Actor (&quot;CalculateOEE&quot;)&#10;— coordinator" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="196" y="300" width="164" height="74" as="geometry" />
</mxCell>
<mxCell id="aa1" value="Alarm Actor (&quot;OverTemp&quot;)&#10;— coordinator (computed)" style="whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="376" y="300" width="164" height="74" as="geometry" />
</mxCell>
<mxCell id="aa2" value="Alarm Actor (&quot;LowPressure&quot;)&#10;— coordinator (computed)" style="whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="556" y="300" width="164" height="74" as="geometry" />
</mxCell>
<mxCell id="naa1" value="Native Alarm Actor (&quot;OpcUaServer1&quot;)&#10;— read-only mirror, peer to Alarm Actor" style="whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="736" y="300" width="164" height="74" as="geometry" />
</mxCell>
<!-- Execution actors (bottom row, under their coordinators) -->
<mxCell id="sea1" value="Script Execution Actor&#10;— short-lived, per invocation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;dashed=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="16" y="430" width="164" height="64" as="geometry" />
</mxCell>
<mxCell id="sea2" value="Script Execution Actor&#10;— short-lived, per invocation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;dashed=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="196" y="430" width="164" height="64" as="geometry" />
</mxCell>
<mxCell id="aea1" value="Alarm Execution Actor&#10;— short-lived, per on-trigger invocation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;dashed=1;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="376" y="430" width="164" height="64" as="geometry" />
</mxCell>
<!-- Child of MachineA-002 -->
<mxCell id="ia2child" value="… (Script / Alarm Actors)" style="text;html=1;align=center;verticalAlign=middle;fontSize=12;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="992" y="304" width="160" height="40" as="geometry" />
</mxCell>
<!-- Edges: root -> instances -->
<mxCell id="e-dms-ia1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="dms" target="ia1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-dms-ia2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="dms" target="ia2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-dms-more" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;dashed=1;" edge="1" parent="1" source="dms" target="iamore">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Edges: MachineA-001 -> its children (all top-row, no box in between) -->
<mxCell id="e-ia1-sa1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="ia1" target="sa1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-ia1-sa2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="ia1" target="sa2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-ia1-aa1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="ia1" target="aa1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-ia1-aa2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="ia1" target="aa2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-ia1-naa1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="ia1" target="naa1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Edges: coordinator -> execution actor (short vertical, own column) -->
<mxCell id="e-sa1-sea1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="sa1" target="sea1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-sa2-sea2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="sa2" target="sea2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e-aa1-aea1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="aa1" target="aea1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Edge: MachineA-002 -> its child -->
<mxCell id="e-ia2-child" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;dashed=1;" edge="1" parent="1" source="ia2" target="ia2child">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

@@ -0,0 +1,82 @@
<mxfile host="app.diagrams.net">
<diagram id="msglifecycle" name="Message Lifecycle">
<mxGraphModel dx="1000" dy="1300" grid="1" gridSize="10" guides="1" arrows="1"
fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="1300" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- Start -->
<mxCell id="submit" value="Script submits message" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="320" y="40" width="220" height="50" as="geometry" />
</mxCell>
<!-- Attempt immediate delivery -->
<mxCell id="attempt" value="Attempt immediate delivery" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="320" y="140" width="220" height="50" as="geometry" />
</mxCell>
<!-- Decision: success or failure -->
<mxCell id="d1" value="Delivered?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="350" y="240" width="160" height="90" as="geometry" />
</mxCell>
<!-- Success -> Remove from buffer -->
<mxCell id="remove1" value="Remove from buffer" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="620" y="255" width="200" height="60" as="geometry" />
</mxCell>
<!-- Failure -> Buffer message -->
<mxCell id="buffer" value="Buffer message" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="330" y="390" width="200" height="50" as="geometry" />
</mxCell>
<!-- Retry loop -->
<mxCell id="retry" value="Retry loop&#10;(per retry policy)" style="whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="330" y="490" width="200" height="60" as="geometry" />
</mxCell>
<!-- Retry decision -->
<mxCell id="d2" value="Retry outcome" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="350" y="610" width="160" height="100" as="geometry" />
</mxCell>
<!-- Retry success -> Remove from buffer + notify standby -->
<mxCell id="remove2" value="Remove from buffer&#10;+ notify standby" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="620" y="625" width="200" height="70" as="geometry" />
</mxCell>
<!-- Max retries exhausted -> Park message -->
<mxCell id="park" value="Park message&#10;(dead-letter)" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="330" y="770" width="200" height="60" as="geometry" />
</mxCell>
<!-- Edges -->
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="submit" target="attempt">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="attempt" target="d1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" value="Success" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="d1" target="remove1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" value="Failure" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="d1" target="buffer">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="buffer" target="retry">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="retry" target="d2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e7" value="Success" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="d2" target="remove2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e8" value="Max retries exhausted" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;fontSize=12;" edge="1" parent="1" source="d2" target="park">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

@@ -0,0 +1,34 @@
<mxfile host="app.diagrams.net">
<diagram id="transport-architecture" name="Architecture">
<mxGraphModel dx="900" dy="800" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="container" value="ZB.MOM.WW.ScadaBridge.Transport" style="swimlane;whiteSpace=wrap;html=1;startSize=34;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=1;fontSize=14;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="80" y="40" width="560" height="610" as="geometry" />
</mxCell>
<mxCell id="exporter" value="IBundleExporter&#10;ExportAsync(ExportSelection, Passphrase?, ct) → Stream" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;verticalAlign=top;spacingTop=8;" vertex="1" parent="container">
<mxGeometry x="30" y="50" width="500" height="70" as="geometry" />
</mxCell>
<mxCell id="importer" value="IBundleImporter&#10;LoadAsync(stream, Passphrase?, ct) → BundleSession&#10;PreviewAsync(sessionId, ct) → ImportPreview&#10;ApplyAsync(sessionId, resolutions, ct) → ImportResult" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;verticalAlign=top;spacingTop=8;" vertex="1" parent="container">
<mxGeometry x="30" y="140" width="500" height="110" as="geometry" />
</mxCell>
<mxCell id="resolver" value="DependencyResolver" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=12;" vertex="1" parent="container">
<mxGeometry x="30" y="270" width="500" height="40" as="geometry" />
</mxCell>
<mxCell id="serializer" value="BundleSerializer&#10;(manifest + content JSON; ZIP packer)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=12;" vertex="1" parent="container">
<mxGeometry x="30" y="330" width="500" height="40" as="geometry" />
</mxCell>
<mxCell id="encryptor" value="BundleSecretEncryptor&#10;(AES-256-GCM + PBKDF2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;spacingLeft=12;" vertex="1" parent="container">
<mxGeometry x="30" y="390" width="500" height="40" as="geometry" />
</mxCell>
<mxCell id="sessionstore" value="BundleSessionStore&#10;(in-memory, TTL'd)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;spacingLeft=12;" vertex="1" parent="container">
<mxGeometry x="30" y="450" width="500" height="40" as="geometry" />
</mxCell>
<mxCell id="manifestvalidator" value="ManifestValidator&#10;(schema/version gating, hash check)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;spacingLeft=12;" vertex="1" parent="container">
<mxGeometry x="30" y="510" width="500" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

@@ -0,0 +1,85 @@
<mxfile host="app.diagrams.net">
<diagram id="transport-export-flow" name="Export Flow (Backend)">
<mxGraphModel dx="1000" dy="800" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="user" value="User (Design role)" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="160" height="50" as="geometry" />
</mxCell>
<mxCell id="wizard" value="Central UI Export wizard" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="50" as="geometry" />
</mxCell>
<mxCell id="exporter" value="IBundleExporter" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="280" y="150" width="200" height="50" as="geometry" />
</mxCell>
<mxCell id="resolver" value="DependencyResolver" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="280" y="250" width="200" height="40" as="geometry" />
</mxCell>
<mxCell id="repos" value="repositories (read)" style="shape=cylinder3;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="600" y="245" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="serializer" value="EntitySerializer" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="280" y="310" width="200" height="40" as="geometry" />
</mxCell>
<mxCell id="contentjson" value="content.json" style="shape=note;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;size=12;" vertex="1" parent="1">
<mxGeometry x="610" y="310" width="110" height="40" as="geometry" />
</mxCell>
<mxCell id="encryptor" value="BundleSecretEncryptor" style="whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="280" y="370" width="200" height="40" as="geometry" />
</mxCell>
<mxCell id="contentenc" value="content.enc&#10;(if passphrase)" style="shape=note;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;size=12;" vertex="1" parent="1">
<mxGeometry x="610" y="368" width="110" height="44" as="geometry" />
</mxCell>
<mxCell id="manifestbuilder" value="ManifestBuilder" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="280" y="430" width="200" height="40" as="geometry" />
</mxCell>
<mxCell id="manifestjson" value="manifest.json" style="shape=note;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;size=12;" vertex="1" parent="1">
<mxGeometry x="610" y="430" width="110" height="40" as="geometry" />
</mxCell>
<mxCell id="zip" value="ZIP packer → temp file → browser download" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="240" y="520" width="280" height="50" as="geometry" />
</mxCell>
<mxCell id="audit" value="IAuditService.LogAsync(BundleExported …)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="240" y="620" width="280" height="50" as="geometry" />
</mxCell>
<mxCell id="e0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="user" target="wizard">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="wizard" target="exporter">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="exporter" target="resolver">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="resolver" target="serializer">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="serializer" target="encryptor">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="encryptor" target="manifestbuilder">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="manifestbuilder" target="zip">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="zip" target="audit">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="b1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="resolver" target="repos">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="b2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="serializer" target="contentjson">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="b3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="encryptor" target="contentenc">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="b4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="manifestbuilder" target="manifestjson">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

@@ -0,0 +1,49 @@
<mxfile host="app.diagrams.net">
<diagram id="transport-import-flow" name="Import Flow (Backend)">
<mxGraphModel dx="1000" dy="900" grid="1" gridSize="10" guides="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="user" value="User (Admin role) ─► uploads bundle" style="rounded=1;arcSize=30;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="260" y="40" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="load" value="IBundleImporter.LoadAsync&#10;&#160;&#160;· verify SHA-256 (manifest vs content)&#10;&#160;&#160;· check bundleFormatVersion supported&#10;&#160;&#160;· decrypt content.enc with passphrase (if encrypted)&#10;&#160;&#160;· deserialize entities&#10;&#160;&#160;· open BundleSession (30-min TTL)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=12;verticalAlign=top;spacingTop=8;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="220" y="150" width="400" height="140" as="geometry" />
</mxCell>
<mxCell id="preview" value="PreviewAsync → diff vs target DB → ImportPreview" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="220" y="350" width="400" height="50" as="geometry" />
</mxCell>
<mxCell id="review" value="(user reviews + resolves conflicts)" style="whiteSpace=wrap;html=1;fillColor=none;strokeColor=none;fontStyle=2;align=left;" vertex="1" parent="1">
<mxGeometry x="630" y="425" width="240" height="30" as="geometry" />
</mxCell>
<mxCell id="apply" value="ApplyAsync (single EF transaction)&#10;&#160;&#160;· run two-tier semantic validation&#10;&#160;&#160;&#160;&#160;(minimal name scan + full SemanticValidator)&#10;&#160;&#160;· apply resolutions (add / overwrite / skip / rename)&#10;&#160;&#160;· upsert TemplateFolder hierarchy&#10;&#160;&#160;· IAuditService.LogAsync(BundleImported …)&#10;&#160;&#160;· commit" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;spacingLeft=12;verticalAlign=top;spacingTop=8;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="220" y="460" width="400" height="160" as="geometry" />
</mxCell>
<mxCell id="result" value="ImportResult → UI step 5" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="260" y="680" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="deployments" value="&quot;View on Deployments →&quot; (existing page)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="260" y="780" width="320" height="50" as="geometry" />
</mxCell>
<mxCell id="e0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="user" target="load">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="load" target="preview">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="preview" target="apply">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e2b" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=none;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="preview" target="review">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="apply" target="result">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" edge="1" parent="1" source="result" target="deployments">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB