{ "ScadaBridge": { "_nodeName": "Host-018: NodeName stamps SourceNode on AuditLog/Notifications/SiteCalls rows (CLAUDE.md 'Centralized Audit Log' decision) and backs IX_AuditLog_Node_Occurred. Convention: 'node-a'/'node-b' for site nodes, 'central-a'/'central-b' for central nodes. Override per-node in multi-node deployments (the docker per-node configs do this). When left at the default below, single-node dev rows are stamped with 'node-a'; an empty value normalises to a NULL SourceNode.", "Node": { "Role": "Site", "NodeHostname": "localhost", "SiteId": "site-a", "RemotingPort": 8082, "GrpcPort": 8083, "MetricsPort": 8084, "NodeName": "node-a" }, "Cluster": { "_seedNodes": "Host-0xx: second entry is the FUTURE node-b remoting port (8085) for a two-node localhost site. It must be an Akka remoting endpoint — never this node's GrpcPort (8083) or MetricsPort (8084); StartupValidator rejects both.", "SeedNodes": [ "akka.tcp://scadabridge@localhost:8082", "akka.tcp://scadabridge@localhost:8085" ], "SplitBrainResolverStrategy": "auto-down", "StableAfter": "00:00:15", "HeartbeatInterval": "00:00:02", "FailureDetectionThreshold": "00:00:10", "MinNrOfMembers": 1, "_bootstrapGuard": "Gitea #33 simultaneous-cold-start split-brain guard. Dark switch, default off (Akka's config-driven self-first auto-join, unchanged). Enable ONLY where both pair VMs can power up together (shared power/hypervisor event) with no start-order serialization: then the lower host:port founds self-first and the higher probes-then-joins, so the pair converges to one cluster instead of two 1-node clusters. Timings validated > 0 when enabled.", "BootstrapGuard": { "Enabled": false, "PartnerProbeSeconds": 25, "PartnerProbeIntervalMs": 500, "ProbeConnectTimeoutMs": 1000 } }, "Database": { // Migration-only as of LocalDb Phase 2. The site config tables now live in the // consolidated LocalDb database (LocalDb:Path). SiteDbPath is read once at boot to drain // a pre-Phase-2 scadabridge.db, and is unused after that - keep it until this node has // started once. "SiteDbPath": "./data/scadabridge.db" }, "DataConnection": { "ReconnectInterval": "00:00:05", "TagResolutionRetryInterval": "00:00:10", "WriteTimeout": "00:00:30" }, "StoreAndForward": { // Migration-only as of LocalDb Phase 2. The store-and-forward buffer now lives in the // consolidated LocalDb database (LocalDb:Path) as the replicated sf_messages table. // SqliteDbPath is read once at boot by SiteLocalDbLegacyMigrator to drain a pre-Phase-2 // file, and is unused after that - keep it until this node has started once. "SqliteDbPath": "./data/store-and-forward.db" }, "Communication": { "_grpcPsk": "REQUIRED on Site nodes (StartupValidator fails the boot without it). The preshared key the gRPC control plane authenticates with: ControlPlaneAuthInterceptor is fail-closed, so an unset key refuses every SiteStream call — live subscriptions, audit pulls, cached-telemetry ingest — while the node still reports healthy. Supply it as ${secret:SB-GRPC-PSK-} so the plaintext never sits in this file, and seed the SAME value on central: either as the secret SB-GRPC-PSK- in its store, or as ScadaBridge:Communication:SitePsks:. BOTH nodes of the pair carry the same key. Distinct from LocalDb:Replication:ApiKey, which authenticates the pair partner, not central — never share the two.", "GrpcPsk": "${secret:SB-GRPC-PSK-site-1}", "_centralGrpcEndpoints": "gRPC (CentralControlService) is the only site→central transport since the ClusterClient→gRPC migration's Phase 4. Each entry MUST be a central node's gRPC (h2c) endpoint on its CentralGrpcPort (default 8083) — NOT this site's own gRPC port, and NOT via Traefik (HTTP/1 only). The single dev-loopback default below points only at central-a (localhost:8083). In a multi-central deployment add the second central node here (e.g. 'http://central-b-host:8083') so the channel pair can fail over when central-a is down. StartupValidator requires a Site node to list at least one endpoint.", "CentralGrpcEndpoints": [ "http://localhost:8083" ], "DeploymentTimeout": "00:02:00", "LifecycleTimeout": "00:00:30", "QueryTimeout": "00:00:30", "TransportHeartbeatInterval": "00:00:05", "TransportFailureThreshold": "00:00:15" }, "HealthMonitoring": { "ReportInterval": "00:00:30", "OfflineTimeout": "00:01:00" }, "SiteEventLog": { "RetentionDays": 30, "MaxStorageMb": 1024, "PurgeScheduleCron": "0 2 * * *" }, "Notification": {}, "Logging": { "MinimumLevel": "Information" } }, // Consolidated site database (LocalDb Phase 1): OperationTracking + site_events. // LocalDb:Path is REQUIRED and validated on start - a site node with no value here // fails to boot, so every site config must set it. "LocalDb": { "Path": "./data/site-localdb.db" }, // arch-review remediation WP1.2: the site hot-path audit writer's SQLite file has no // default path (mirrors LocalDb:Path above) - AuditLog:SiteWriter:DatabasePath is // REQUIRED and validated pre-host on Site nodes (StartupValidator), the same way // LocalDb:Path is, so every site config must set it. "AuditLog": { "SiteWriter": { "DatabasePath": "./data/auditlog.db" } } }