# gRPC Streaming Channel Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

**Goal:** Replace ClusterClient-based debug event streaming with a dedicated gRPC server-streaming channel from site nodes to central, following the design in `docs/plans/grpc_streams.md`.

**Architecture:** Each site node runs a gRPC server (`SiteStreamGrpcServer`) on a dedicated HTTP/2 port. Central creates per-site gRPC clients (`SiteStreamGrpcClient`) that open server-streaming subscriptions filtered by instance name. Events flow: SiteStreamManager → relay actor → Channel\<T\> → gRPC stream → central callback → DebugStreamBridgeActor → SignalR/Blazor. ClusterClient continues handling command/control (subscribe/unsubscribe/snapshot).

**Tech Stack:** .NET 10, gRPC (Grpc.AspNetCore + Grpc.Net.Client), Protocol Buffers, Akka.NET, ASP.NET Core Kestrel, System.Threading.Channels

**Design Reference:** `docs/plans/grpc_streams.md` — full architecture, proto definition, failover, keepalive, backpressure, and review notes.

---

### Task 0: Proto Definition & Stub Generation

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto`
- Create: `src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/` (generated stubs)
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj`

**Step 1: Create the proto file**

Create `src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto` with the proto definition from `docs/plans/grpc_streams.md` "Proto Improvements" section (V1 review notes version with enums and `google.protobuf.Timestamp`):

```protobuf
syntax = "proto3";
option csharp_namespace = "ZB.MOM.WW.ScadaBridge.Communication.Grpc";
package sitestream;

import "google/protobuf/timestamp.proto";

service SiteStreamService {
  rpc SubscribeInstance(InstanceStreamRequest) returns (stream SiteStreamEvent);
}

message InstanceStreamRequest {
  string correlation_id = 1;
  string instance_unique_name = 2;
}

message SiteStreamEvent {
  string correlation_id = 1;
  oneof event {
    AttributeValueUpdate attribute_changed = 2;
    AlarmStateUpdate alarm_changed = 3;
  }
}

enum Quality {
  QUALITY_UNSPECIFIED = 0;
  QUALITY_GOOD = 1;
  QUALITY_UNCERTAIN = 2;
  QUALITY_BAD = 3;
}

enum AlarmStateEnum {
  ALARM_STATE_UNSPECIFIED = 0;
  ALARM_STATE_NORMAL = 1;
  ALARM_STATE_ACTIVE = 2;
}

message AttributeValueUpdate {
  string instance_unique_name = 1;
  string attribute_path = 2;
  string attribute_name = 3;
  string value = 4;
  Quality quality = 5;
  google.protobuf.Timestamp timestamp = 6;
}

message AlarmStateUpdate {
  string instance_unique_name = 1;
  string alarm_name = 2;
  AlarmStateEnum state = 3;
  int32 priority = 4;
  google.protobuf.Timestamp timestamp = 5;
}
```

**Step 2: Add gRPC NuGet packages**

Add to `src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj`:
```xml
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
<PackageReference Include="Grpc.Tools" Version="2.71.0" PrivateAssets="All" />
```

Also add `<FrameworkReference Include="Microsoft.AspNetCore.App" />` if not already present (needed for `Grpc.AspNetCore`).

**Step 3: Generate C# stubs**

Run `protoc` locally to generate stubs. Check generated files into `src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/` — pre-generated and checked in, no `protoc` at build time.

**Step 4: Verify build**

Run: `dotnet build src/ZB.MOM.WW.ScadaBridge.Communication/`
Expected: Build succeeded, 0 errors

**Step 5: Write proto roundtrip tests**

Create `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/ProtoRoundtripTests.cs`:
- Test `AttributeValueUpdate` serialization/deserialization with all Quality enum values
- Test `AlarmStateUpdate` serialization/deserialization with all AlarmStateEnum values
- Test `SiteStreamEvent` oneof discrimination (attribute vs alarm)
- Test `google.protobuf.Timestamp` conversion to/from `DateTimeOffset`

**Step 6: Run tests**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`
Expected: All pass including new proto tests

**Step 7: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Communication/Protos/ src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/ src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/
git commit -m "feat: add sitestream.proto definition and generated gRPC stubs"
```

---

### Task 1: Site Config — GrpcPort in NodeOptions

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Host/NodeOptions.cs:8`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:43-48`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json:7`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Host.Tests/`

**Step 1: Write failing test for GrpcPort validation**

Add to existing startup validator tests: test that a site node with `GrpcPort` outside 1-65535 fails validation, and that a valid `GrpcPort` passes.

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests/`
Expected: New test FAILS (GrpcPort not validated yet)

**Step 3: Add GrpcPort to NodeOptions**

In `src/ZB.MOM.WW.ScadaBridge.Host/NodeOptions.cs`, add:
```csharp
public int GrpcPort { get; set; } = 8083;
```

**Step 4: Add validation in StartupValidator**

In `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs`, after the existing site validation block (line ~43):
```csharp
if (role == "Site")
{
    var grpcPortStr = nodeSection["GrpcPort"];
    if (grpcPortStr != null && (!int.TryParse(grpcPortStr, out var gp) || gp < 1 || gp > 65535))
        errors.Add("ScadaBridge:Node:GrpcPort must be 1-65535");
}
```

**Step 5: Add GrpcPort to appsettings.Site.json**

In `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json`, add after `"RemotingPort": 8082`:
```json
"GrpcPort": 8083
```

**Step 6: Run tests**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests/`
Expected: All pass

**Step 7: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Host/ tests/ZB.MOM.WW.ScadaBridge.Host.Tests/
git commit -m "feat: add GrpcPort config to NodeOptions with startup validation"
```

---

### Task 2: Site Entity — gRPC Address Fields

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Sites/Site.cs:9-10`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SiteCommands.cs:5-6`
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/` (migration)
- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs` (handlers)
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/SiteCommands.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/Sites.razor`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/`

**Step 1: Add fields to Site entity**

In `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Sites/Site.cs`, add after `NodeBAddress`:
```csharp
public string? GrpcNodeAAddress { get; set; }
public string? GrpcNodeBAddress { get; set; }
```

**Step 2: Update management commands**

In `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SiteCommands.cs`, add `GrpcNodeAAddress` and `GrpcNodeBAddress` optional params to `CreateSiteCommand` and `UpdateSiteCommand`.

**Step 3: Add EF Core migration**

Run: `dotnet ef migrations add AddGrpcNodeAddresses --project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ --startup-project src/ZB.MOM.WW.ScadaBridge.Host/`

Or create manual migration adding nullable `GrpcNodeAAddress` and `GrpcNodeBAddress` string columns to Sites table.

**Step 4: Update ManagementActor handlers**

In `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs`, update `HandleCreateSite` and `HandleUpdateSite` to pass gRPC addresses to the repository.

**Step 5: Update CLI SiteCommands**

In `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/SiteCommands.cs`, add `--grpc-node-a-address` and `--grpc-node-b-address` options to `site create` and `site update` commands.

**Step 6: Update Central UI Sites.razor**

In `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/Sites.razor`:
- Add `_formGrpcNodeAAddress` and `_formGrpcNodeBAddress` form fields
- Add table columns for gRPC addresses
- Wire into create/update handlers

**Step 7: Run tests**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests/`
Expected: All pass

**Step 8: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Commons/ src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ src/ZB.MOM.WW.ScadaBridge.ManagementService/ src/ZB.MOM.WW.ScadaBridge.CLI/ src/ZB.MOM.WW.ScadaBridge.CentralUI/
git commit -m "feat: add GrpcNodeAAddress/GrpcNodeBAddress to Site entity, CLI, and UI"
```

---

### Task 3: Site-Side gRPC Server — StreamRelayActor

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/StreamRelayActor.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs`

**Step 1: Write failing test**

Test that `StreamRelayActor` receives `AttributeValueChanged` and writes a correctly-converted `SiteStreamEvent` proto message to a `ChannelWriter<SiteStreamEvent>`. Use Akka.TestKit.

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`
Expected: FAIL (class doesn't exist)

**Step 3: Implement StreamRelayActor**

Create `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/StreamRelayActor.cs`:
- `ReceiveActor` that receives `AttributeValueChanged` and `AlarmStateChanged`
- Converts each to the proto `SiteStreamEvent` with correct enum mappings and `Timestamp` conversion
- Writes to `ChannelWriter<SiteStreamEvent>` via `TryWrite`
- Logs dropped events when channel is full

**Step 4: Run tests**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`
Expected: All pass

**Step 5: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/ tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/
git commit -m "feat: add StreamRelayActor bridging Akka events to gRPC proto channel"
```

---

### Task 4: Site-Side gRPC Server — SiteStreamGrpcServer

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcServerTests.cs`

**Step 1: Write failing tests**

- Server accepts subscription, relays events from mock SiteStreamManager to gRPC stream
- Server cleans up SiteStreamManager subscription on cancellation
- Server rejects duplicate `correlation_id` (cancels old stream)
- Server enforces max concurrent streams (100), rejects with `ResourceExhausted`
- Server rejects with `Unavailable` before actor system is ready

**Step 2: Run tests to verify they fail**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`
Expected: FAIL

**Step 3: Implement SiteStreamGrpcServer**

Create `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs`:
- Inherits `SiteStreamService.SiteStreamServiceBase`
- Injects `SiteStreamManager` (or interface), `ActorSystem`
- Tracks active streams in `ConcurrentDictionary<string, CancellationTokenSource>`
- `SubscribeInstance`: creates `Channel<SiteStreamEvent>`, creates `StreamRelayActor`, subscribes to SiteStreamManager, reads channel → writes to gRPC response stream
- `finally`: removes subscription, stops relay actor, removes from active streams
- Readiness gate: checks `ActorSystem` availability before accepting

**Step 4: Run tests**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`
Expected: All pass

**Step 5: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/ tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/
git commit -m "feat: add SiteStreamGrpcServer with Channel<T> bridge and stream limits"
```

---

### Task 5: Switch Site Host to WebApplicationBuilder + gRPC

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:157-174`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json`
- Modify: `docker/docker-compose.yml`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Host.Tests/`

**Step 1: Write failing test**

Site host startup test: verify `WebApplicationBuilder` starts, gRPC port is configured, `MapGrpcService` is registered.

**Step 2: Switch site host from generic Host to WebApplicationBuilder**

In `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs`, replace the `Host.CreateDefaultBuilder()` site section with `WebApplication.CreateBuilder()` + Kestrel HTTP/2 on `GrpcPort` + `AddGrpc()` + `MapGrpcService<SiteStreamGrpcServer>()`. Keep all existing service registrations via `SiteServiceRegistration.Configure()`.

Add gRPC keepalive settings from `CommunicationOptions`:
- `KeepAlivePingDelay = 15s`
- `KeepAlivePingTimeout = 10s`

**Step 3: Update docker-compose.yml**

Expose gRPC port 8083 for each site node:
- Site-A: `9023:8083` / `9024:8083`
- Site-B: `9033:8083` / `9034:8083`
- Site-C: `9043:8083` / `9044:8083`

**Step 4: Add gRPC keepalive config to CommunicationOptions**

Add to `src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptions.cs`:
```csharp
public TimeSpan GrpcKeepAlivePingDelay { get; set; } = TimeSpan.FromSeconds(15);
public TimeSpan GrpcKeepAlivePingTimeout { get; set; } = TimeSpan.FromSeconds(10);
public TimeSpan GrpcMaxStreamLifetime { get; set; } = TimeSpan.FromHours(4);
public int GrpcMaxConcurrentStreams { get; set; } = 100;
```

**Step 5: Run tests and build**

Run: `dotnet build src/ZB.MOM.WW.ScadaBridge.Host/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests/`
Expected: All pass

**Step 6: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Host/ src/ZB.MOM.WW.ScadaBridge.Communication/ docker/
git commit -m "feat: switch site host to WebApplicationBuilder with Kestrel gRPC server"
```

---

### Task 6: Central-Side gRPC Client

**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs`
- Create: `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcClientTests.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcClientFactoryTests.cs`

**Step 1: Write failing tests for SiteStreamGrpcClient**

- Client connects, reads stream, converts proto→domain types (`AttributeValueChanged`, `AlarmStateChanged`), invokes callback
- Client handles stream errors (throws on `RpcException`)
- Client cancellation stops the background reader

**Step 2: Implement SiteStreamGrpcClient**

- Creates `GrpcChannel` with keepalive settings from `CommunicationOptions`
- `SubscribeAsync`: calls `SiteStreamService.SubscribeInstance()`, launches background task to read `ResponseStream`, converts proto→domain, invokes callback
- `Unsubscribe`: cancels the `CancellationTokenSource` for the subscription
- `IAsyncDisposable`: disposes channel

**Step 3: Write failing tests for SiteStreamGrpcClientFactory**

- Creates and caches per-site clients
- Falls back to NodeB on NodeA connection failure
- Disposes clients on site removal

**Step 4: Implement SiteStreamGrpcClientFactory**

- `GetOrCreateAsync(siteIdentifier, grpcNodeAAddress, grpcNodeBAddress)` → `SiteStreamGrpcClient`
- Caches by `siteIdentifier` in `ConcurrentDictionary`
- Manages `GrpcChannel` lifecycle

**Step 5: Run tests**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`
Expected: All pass

**Step 6: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/ tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/
git commit -m "feat: add SiteStreamGrpcClient and SiteStreamGrpcClientFactory"
```

---

### Task 7: Update DebugStreamBridgeActor to Use gRPC

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/DebugStreamService.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/ServiceCollectionExtensions.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`

**Step 1: Write failing tests for updated bridge actor**

- Bridge actor sends subscribe via ClusterClient, receives snapshot
- After snapshot, opens gRPC stream via `SiteStreamGrpcClient`
- Events from gRPC callback forwarded to `_onEvent`
- On gRPC stream error: reconnects to other node with backoff (max 3 retries)
- On stop: cancels gRPC + sends unsubscribe via ClusterClient
- Handles `DebugStreamTerminated` idempotently

**Step 2: Update DebugStreamBridgeActor**

Rewrite to:
1. `PreStart`: send `SubscribeDebugViewRequest` via ClusterClient (unchanged)
2. On `DebugViewSnapshot` received: open gRPC stream first (per handoff race mitigation — stream first, then apply snapshot)
3. gRPC callback delivers events to `Self` via `Tell` (marshals onto actor thread)
4. On gRPC error: enter reconnecting state, try other node, backoff, max retries
5. On stop: cancel gRPC subscription + send `UnsubscribeDebugViewRequest`

**Step 3: Update DebugStreamService**

Inject `SiteStreamGrpcClientFactory`. Resolve `GrpcNodeAAddress`/`GrpcNodeBAddress` from `Site` entity. Pass to bridge actor.

**Step 4: Register factory in DI**

In `src/ZB.MOM.WW.ScadaBridge.Communication/ServiceCollectionExtensions.cs`:
```csharp
services.AddSingleton<SiteStreamGrpcClientFactory>();
```

**Step 5: Run tests**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/`
Expected: All pass

**Step 6: Commit**

```bash
git add src/ZB.MOM.WW.ScadaBridge.Communication/ tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/
git commit -m "feat: update DebugStreamBridgeActor to use gRPC for streaming events"
```

---

### Task 8: Remove ClusterClient Streaming Path

**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs`
- Delete: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugStreamEvent.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorIntegrationTests.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ArchitecturalConstraintTests.cs`

**Step 1: Remove DebugStreamEvent from InstanceActor**

Remove `_debugSubscriberCorrelationIds`, `_siteCommActor`, and all `DebugStreamEvent` forwarding from `PublishAndNotifyChildren` and `HandleAlarmStateChanged`. InstanceActor just publishes to `SiteStreamManager` — the gRPC server picks up events from there.

Keep `HandleSubscribeDebugView` (for snapshot) and `HandleUnsubscribeDebugView`.

**Step 2: Remove DebugStreamEvent from SiteCommunicationActor**

Remove `Receive<DebugStreamEvent>` handler.

**Step 3: Remove DebugStreamEvent from CentralCommunicationActor**

Remove `Receive<DebugStreamEvent>` handler and `HandleDebugStreamEvent` method.

**Step 4: Delete DebugStreamEvent.cs**

Delete `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugStreamEvent.cs`.

**Step 5: Update InstanceActorIntegrationTests**

Remove `DebugStreamEventForwarder` test helper. Update debug subscriber tests to verify events reach `SiteStreamManager` only.

**Step 6: Add architectural constraint test**

In `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ArchitecturalConstraintTests.cs`, add test verifying `DebugStreamEvent` type no longer exists in the Commons assembly.

**Step 7: Run full test suite**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests/`
Expected: All pass

**Step 8: Commit**

```bash
git add -A
git commit -m "refactor: remove ClusterClient streaming path (DebugStreamEvent), events flow via gRPC"
```

---

### Task 9: Docker & End-to-End Integration Test

**Files:**
- Modify: `docker/docker-compose.yml`
- Modify: `docker/deploy.sh` (if needed)
- Create: `tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/Grpc/GrpcStreamIntegrationTests.cs`

**Step 1: Update docker-compose site appsettings**

Ensure site container configs include `GrpcPort: 8083` and gRPC ports are exposed.

**Step 2: Write integration test**

End-to-end: start in-process site gRPC server → central gRPC client → verify event delivery, cancellation cleanup.

**Step 3: Build and deploy cluster**

Run: `bash docker/deploy.sh`
Expected: All containers start, gRPC ports exposed

**Step 4: Manual end-to-end verification**

Run: `timeout 35 dotnet run --project src/ZB.MOM.WW.ScadaBridge.CLI -- --url http://localhost:9000 --username multi-role --password password debug stream --id 1 --format table`

Expected: Initial snapshot + streaming ATTR/ALARM rows via gRPC (not ClusterClient).

Write an OPC UA tag to verify:
```bash
python3 infra/tools/opcua_tool.py write --node "ns=3;s=JoeAppEngine.BTCS" --value "gRPC streaming test" --type String
```

**Step 5: Commit**

```bash
git add tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/ docker/
git commit -m "test: add gRPC stream integration test and docker config"
```

---

### Task 10: Documentation Updates

**Files:**
- Modify: `docs/requirements/HighLevelReqs.md`
- Modify: `docs/requirements/Component-Communication.md`
- Modify: `docs/requirements/Component-SiteRuntime.md`
- Modify: `docs/requirements/Component-Host.md`
- Modify: `docs/requirements/Component-CentralUI.md`
- Modify: `docs/requirements/Component-CLI.md`
- Modify: `docs/requirements/Component-ConfigurationDatabase.md`
- Modify: `docs/requirements/Component-ClusterInfrastructure.md`
- Modify: `CLAUDE.md`
- Modify: `README.md`
- Modify: `docker/README.md`

**Step 1: Update HighLevelReqs.md**

Section 5 (Communication): Add gRPC streaming transport. ClusterClient for command/control, gRPC for real-time data.

**Step 2: Update Component-Communication.md**

Pattern 6: Replace ClusterClient streaming with gRPC. Add SiteStreamGrpcServer, SiteStreamGrpcClient, SiteStreamGrpcClientFactory. Add gRPC keepalive config.

**Step 3: Update remaining component docs**

Per the documentation update table in `docs/plans/grpc_streams.md` § Documentation Updates.

**Step 4: Update CLAUDE.md**

Add under Data & Communication: "gRPC streaming for site→central real-time data; ClusterClient for command/control only"

**Step 5: Update README.md architecture diagram**

Add gRPC streaming channel between site and central in the ASCII diagram.

**Step 6: Update docker/README.md**

Add gRPC ports to port allocation table.

**Step 7: Commit**

```bash
git add docs/ CLAUDE.md README.md docker/README.md
git commit -m "docs: update requirements and architecture for gRPC streaming channel"
```

---

### Task 11: Final Guardrail Tests

**Files:**
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/ProtoContractTests.cs`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/CleanupVerificationTests.cs`

**Step 1: Proto contract test**

Verify all `oneof` variants in `SiteStreamEvent` have corresponding handlers in `StreamRelayActor` and `ConvertToDomainEvent`. If a new proto field is added without handlers, the test fails.

**Step 2: Cleanup verification test**

Verify that after gRPC stream cancellation, `SiteStreamManager.SubscriptionCount` returns to zero (no leaked subscriptions).

**Step 3: No ClusterClient streaming regression test**

Integration test that subscribes via gRPC, triggers changes, and verifies events arrive via gRPC — NOT via `DebugStreamEvent`.

**Step 4: Run full test suite**

Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/ && dotnet test tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/`
Expected: All pass, zero warnings

**Step 5: Commit**

```bash
git add tests/
git commit -m "test: add proto contract, cleanup verification, and regression guardrail tests"
```
