@yarkivaev/scada 1.5.0 → 2.3.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +196 -33
  2. package/db/migrations/C0001__central_drop_metrics.sql +1 -0
  3. package/db/migrations/C0002__central_revoke_delete.sql +12 -0
  4. package/db/migrations/C0003__central_operators.sql +7 -0
  5. package/db/migrations/C0004__central_operations_grants.sql +2 -0
  6. package/db/migrations/C0005__central_operators_grants.sql +1 -0
  7. package/db/migrations/C0006__central_operators_registration.sql +16 -0
  8. package/db/migrations/E0001__edge_metrics.sql +8 -0
  9. package/db/migrations/E0002__edge_retention_delete.sql +8 -0
  10. package/db/migrations/E0003__edge_operations_grants.sql +2 -0
  11. package/db/migrations/V0001__baseline.sql +25 -0
  12. package/db/migrations/V0002__sink_extension_tables.sql +6 -0
  13. package/db/migrations/V0003__operations.sql +10 -0
  14. package/db/migrations/V0004__ingest_checkpoints.sql +6 -0
  15. package/db/migrations/V0005__user_decisions_operator.sql +2 -0
  16. package/index.js +75 -46
  17. package/package.json +28 -9
  18. package/src/application/bindSilentStreams.js +20 -0
  19. package/src/application/edgeOperatorCatalog.js +45 -0
  20. package/src/application/export/exportJob.js +118 -0
  21. package/src/application/export/exportQuery.js +32 -0
  22. package/src/application/export/exportSink.js +25 -0
  23. package/src/application/export/exportStream.js +33 -0
  24. package/src/application/machineInPlant.js +20 -0
  25. package/src/application/metricsPlant.js +35 -0
  26. package/src/application/plantApi.js +49 -0
  27. package/src/application/plantOperations.js +19 -0
  28. package/src/application/plantServer.js +107 -0
  29. package/src/application/shopWithTimeline.js +112 -0
  30. package/src/application/siteOperatorCatalog.js +78 -0
  31. package/src/application/siteServer.js +191 -0
  32. package/src/application/supervisorSink.js +97 -0
  33. package/src/application/timelineOperatorFromEnv.js +19 -0
  34. package/src/bin/supervisor-sink.js +28 -0
  35. package/src/{alert.js → domain/alerting/alert.js} +2 -2
  36. package/src/{alerts.js → domain/alerting/alerts.js} +3 -3
  37. package/src/domain/alerting/ingest.js +18 -0
  38. package/src/domain/ingest/ingestCursor.js +26 -0
  39. package/src/domain/operation/operation.js +23 -0
  40. package/src/domain/operation/operations.js +81 -0
  41. package/src/domain/operator/operator.js +23 -0
  42. package/src/domain/plant/machine.js +32 -0
  43. package/src/domain/plant/plant.js +20 -0
  44. package/src/domain/plant/shop.js +24 -0
  45. package/src/domain/segment/dispatch.js +31 -0
  46. package/src/domain/segment/normalize.js +31 -0
  47. package/src/domain/segment/silenceBudget.js +16 -0
  48. package/src/{initialized.js → domain/shared/initialized.js} +3 -4
  49. package/src/{pubsub.js → domain/shared/pubsub.js} +2 -3
  50. package/src/domain/timeline/timeline.js +25 -0
  51. package/src/infrastructure/catalog/tag-catalog.json +27 -0
  52. package/src/infrastructure/catalog/tagCatalog.js +65 -0
  53. package/src/infrastructure/client/index.js +20 -0
  54. package/src/infrastructure/client/machineClient.js +159 -0
  55. package/src/infrastructure/client/machineOperationsClient.js +159 -0
  56. package/src/infrastructure/client/scadaClient.js +67 -0
  57. package/src/infrastructure/client/sseConnection.js +42 -0
  58. package/src/infrastructure/http/edge/edgeApi.js +54 -0
  59. package/src/infrastructure/http/edge/httpMetricsRead.js +34 -0
  60. package/src/infrastructure/http/edge/metricsSensor.js +15 -0
  61. package/src/infrastructure/http/edge/parseStateHttpTimeoutMs.js +20 -0
  62. package/src/infrastructure/http/edge/routes/checkpoint/checkpointRoutes.js +79 -0
  63. package/src/infrastructure/http/edge/routes/metrics/metricsBatch.js +59 -0
  64. package/src/infrastructure/http/edge/routes/metrics/metricsCurrent.js +29 -0
  65. package/src/infrastructure/http/edge/routes/metrics/metricsPoll.js +25 -0
  66. package/src/infrastructure/http/edge/routes/metrics/metricsRange.js +26 -0
  67. package/src/infrastructure/http/edge/routes/metrics/metricsRoutes.js +21 -0
  68. package/src/infrastructure/http/edge/routes/retention/retentionRoutes.js +41 -0
  69. package/src/infrastructure/http/edge/startTestEdgeApi.js +39 -0
  70. package/src/infrastructure/http/edge/stateAccess.js +13 -0
  71. package/src/infrastructure/http/edge/stateHttpClient.js +106 -0
  72. package/src/infrastructure/http/edge/stateHttpTimeoutError.js +13 -0
  73. package/src/infrastructure/http/plant/json/decisionJson.js +44 -0
  74. package/src/infrastructure/http/plant/json/operationJson.js +28 -0
  75. package/src/infrastructure/http/plant/json/operatorJson.js +22 -0
  76. package/src/infrastructure/http/plant/json/segmentJson.js +27 -0
  77. package/src/infrastructure/http/plant/operationAudit.js +56 -0
  78. package/src/infrastructure/http/plant/routes/alertRoute.js +93 -0
  79. package/src/infrastructure/http/plant/routes/catalogRoute.js +21 -0
  80. package/src/infrastructure/http/plant/routes/decisionRoute.js +60 -0
  81. package/src/infrastructure/http/plant/routes/machineRoute.js +35 -0
  82. package/src/infrastructure/http/plant/routes/measurementRoute.js +60 -0
  83. package/src/infrastructure/http/plant/routes/operationDrafts.js +79 -0
  84. package/src/infrastructure/http/plant/routes/operationRoute.js +80 -0
  85. package/src/infrastructure/http/plant/routes/operationWrites.js +186 -0
  86. package/src/infrastructure/http/plant/routes/operatorRoute.js +143 -0
  87. package/src/infrastructure/http/plant/routes/simulationRoute.js +61 -0
  88. package/src/infrastructure/http/plant/routes/timelineRoute.js +112 -0
  89. package/src/infrastructure/http/plant/streams/alertStream.js +68 -0
  90. package/src/infrastructure/http/plant/streams/heartbeatStream.js +34 -0
  91. package/src/infrastructure/http/plant/streams/measurementStream.js +90 -0
  92. package/src/infrastructure/http/plant/streams/operationStream.js +42 -0
  93. package/src/infrastructure/http/plant/streams/timelineStream.js +97 -0
  94. package/src/infrastructure/http/plant/timelineOperator.js +87 -0
  95. package/src/infrastructure/ingest/activity/activityTracking.js +74 -0
  96. package/src/infrastructure/ingest/activity/activityTransformer.js +45 -0
  97. package/src/infrastructure/ingest/codecs/alertCodec.js +56 -0
  98. package/src/infrastructure/ingest/codecs/segmentCodec.js +30 -0
  99. package/src/infrastructure/ingest/codecs/userDecisionCodec.js +78 -0
  100. package/src/infrastructure/ingest/cooldown.js +24 -0
  101. package/src/infrastructure/ingest/db/migrate.js +78 -0
  102. package/src/infrastructure/ingest/db/migrationProfile.js +35 -0
  103. package/src/infrastructure/ingest/db/runRetention.js +58 -0
  104. package/src/infrastructure/ingest/ingestCheckpoint.js +71 -0
  105. package/src/infrastructure/ingest/modbus/mx210Tcp.js +81 -0
  106. package/src/infrastructure/ingest/modbus/silentStreams.js +152 -0
  107. package/src/infrastructure/ingest/mqtt/metricsTransformer.js +54 -0
  108. package/src/infrastructure/ingest/mqtt/modbusDeviceSpec.js +112 -0
  109. package/src/infrastructure/ingest/mqtt/modbusMqtt.js +204 -0
  110. package/src/infrastructure/ingest/mqtt/mqttMetrics.js +78 -0
  111. package/src/infrastructure/ingest/parseRequestTimeoutMs.js +20 -0
  112. package/src/infrastructure/ingest/pipelines/alertPipeline.js +48 -0
  113. package/src/infrastructure/ingest/pipelines/decisionPipeline.js +45 -0
  114. package/src/infrastructure/ingest/pipelines/segmentPipeline.js +60 -0
  115. package/src/infrastructure/ingest/processingErrorLog.js +25 -0
  116. package/src/infrastructure/ingest/silentOpenWatch.js +50 -0
  117. package/src/infrastructure/ingest/sinks/alertSink.js +43 -0
  118. package/src/infrastructure/ingest/sinks/closeOrphanOpen.js +32 -0
  119. package/src/infrastructure/ingest/sinks/closeSilentOpen.js +39 -0
  120. package/src/infrastructure/ingest/sinks/retagSink.js +43 -0
  121. package/src/infrastructure/ingest/sinks/userDecisionSink.js +41 -0
  122. package/src/infrastructure/ingest/telemetry/amqpMetricsIngest.js +94 -0
  123. package/src/infrastructure/ingest/telemetry/amqpMqttRelay.js +71 -0
  124. package/src/infrastructure/ingest/telemetry/deliverToMqttRecord.js +19 -0
  125. package/src/infrastructure/ingest/telemetry/streamNameFromTopic.js +21 -0
  126. package/src/infrastructure/messaging/ownership/httpOperations.js +96 -0
  127. package/src/infrastructure/messaging/ownership/httpTimeline.js +99 -0
  128. package/src/infrastructure/messaging/ownership/machineOwners.js +39 -0
  129. package/src/infrastructure/messaging/ownership/ownerTimeline.js +28 -0
  130. package/src/infrastructure/messaging/stomp/alerts/stompAlerts.js +21 -0
  131. package/src/infrastructure/messaging/stomp/alerts/stompAlertsCollection.js +51 -0
  132. package/src/infrastructure/messaging/stomp/alerts/stompAlertsInit.js +24 -0
  133. package/src/infrastructure/messaging/stomp/alerts/stompAlertsLogic.js +51 -0
  134. package/src/infrastructure/messaging/stomp/stompTimelineSegments.js +80 -0
  135. package/src/infrastructure/messaging/stomp/timeline.js +21 -0
  136. package/src/infrastructure/messaging/stomp/userDecisionBody.js +25 -0
  137. package/src/infrastructure/messaging/stomp/userDecisions.js +42 -0
  138. package/src/infrastructure/operators/centralOperators.js +64 -0
  139. package/src/infrastructure/operators/edgeOperators.js +39 -0
  140. package/src/infrastructure/operators/operatorById.js +33 -0
  141. package/src/infrastructure/operators/operatorExtras.js +41 -0
  142. package/src/infrastructure/operators/operators.js +32 -0
  143. package/src/infrastructure/operators/operatorsFromSeed.js +18 -0
  144. package/src/infrastructure/operators/operatorsSync.js +57 -0
  145. package/src/infrastructure/persistence/clickhouse/connection.js +58 -0
  146. package/src/infrastructure/persistence/clickhouse/pollTopicCursors.js +48 -0
  147. package/src/{clickhouseSensor.js → infrastructure/persistence/clickhouse/sensor.js} +9 -27
  148. package/src/infrastructure/persistence/clickhouse/streamHub.js +165 -0
  149. package/src/infrastructure/persistence/memory/alerts.js +21 -0
  150. package/src/infrastructure/persistence/memory/checkpoints.js +98 -0
  151. package/src/infrastructure/persistence/memory/metrics.js +69 -0
  152. package/src/infrastructure/persistence/memory/metricsDisabled.js +19 -0
  153. package/src/infrastructure/persistence/memory/operations.js +87 -0
  154. package/src/infrastructure/persistence/memory/segments.js +83 -0
  155. package/src/infrastructure/persistence/memory/timeline.js +56 -0
  156. package/src/infrastructure/persistence/metricsSensor.js +112 -0
  157. package/src/infrastructure/persistence/pg/alerts.js +25 -0
  158. package/src/infrastructure/persistence/pg/checkpoints.js +115 -0
  159. package/src/infrastructure/persistence/pg/metrics.js +73 -0
  160. package/src/infrastructure/persistence/pg/operations.js +95 -0
  161. package/src/infrastructure/persistence/pg/operators.js +118 -0
  162. package/src/infrastructure/persistence/pg/segments.js +48 -0
  163. package/src/infrastructure/persistence/pg/timeline.js +53 -0
  164. package/src/infrastructure/persistence/pg/userDecisions.js +72 -0
  165. package/src/infrastructure/persistence/postgresPool.js +24 -0
  166. package/src/infrastructure/persistence/stateDataFromMemory.js +28 -0
  167. package/src/infrastructure/persistence/stateDataFromPool.js +18 -0
  168. package/src/infrastructure/sync/operationCodec.js +75 -0
  169. package/src/infrastructure/sync/operationSyncIngest.js +82 -0
  170. package/src/infrastructure/sync/operationSyncSink.js +40 -0
  171. package/src/activeMelting.js +0 -54
  172. package/src/completedMelting.js +0 -42
  173. package/src/event.js +0 -24
  174. package/src/events.js +0 -51
  175. package/src/interval.js +0 -18
  176. package/src/machineChronology.js +0 -79
  177. package/src/meltingChronology.js +0 -37
  178. package/src/meltingMachine.js +0 -53
  179. package/src/meltingRuleEngine.js +0 -37
  180. package/src/meltingShop.js +0 -32
  181. package/src/meltings.js +0 -97
  182. package/src/monitoredMeltingMachine.js +0 -33
  183. package/src/plant.js +0 -23
  184. package/src/postgresSensor.js +0 -105
  185. package/src/requests.js +0 -44
  186. package/src/rule.js +0 -33
  187. package/src/rules.js +0 -23
  188. package/src/scyllaSensor.js +0 -54
  189. package/src/segments.js +0 -64
  190. package/src/sqliteSensor.js +0 -106
package/README.md CHANGED
@@ -1,53 +1,216 @@
1
1
  # scada
2
2
 
3
- SCADA domain objects for industrial plant monitoring.
3
+ Generic industrial monitoring domain, state persistence, and HTTP APIs (`@yarkivaev/scada`).
4
+
5
+ This package is plant-agnostic: it defines `plant` → `shop` → `machine` hierarchy, timeline and alerts, sensor read ports, STOMP ingest pipelines, and PostgreSQL/ClickHouse adapters. Plant-specific routes, sensors, HMI strings, and tag taxonomies live in downstream plant packages.
6
+
7
+ Repository: `https://github.com/yarkivaev/scada`
4
8
 
5
9
  ## Install
6
10
 
11
+ ```bash
12
+ npm install @yarkivaev/scada
7
13
  ```
8
- npm install scada
14
+
15
+ Node.js 22+, ES modules.
16
+
17
+ ## Quick start
18
+
19
+ ```javascript
20
+ import { plantApi, metricsPlant, clickhouseConnection } from '@yarkivaev/scada';
21
+
22
+ const p = await metricsPlant(
23
+ [{ device: 'device-1', machine: 'm1' }],
24
+ {
25
+ connection: () => clickhouseConnection(process.env.CLICKHOUSE_HOST),
26
+ buildSensors: (conn, entry) => ({ /* sensor factories */ }),
27
+ shopFactory: (entries, ctx) => { /* return shop */ }
28
+ }
29
+ );
30
+
31
+ const api = plantApi('/api/v1', p);
9
32
  ```
10
33
 
11
- ## Usage
34
+ For a full deployable process, use `siteServer` or the `supervisor-sink` binary (see below). Plant packages wrap these with site-specific routes and sensors.
35
+
36
+ ## Layout
37
+
38
+ ```
39
+ scada/
40
+ src/
41
+ domain/ business objects and ingest policies (no I/O)
42
+ application/ composition roots (plantApi, supervisorSink, siteServer)
43
+ infrastructure/ adapters (http, persistence, messaging, ingest, client)
44
+ bin/ process entry points
45
+ test/ mirrors src/ (domain/, infrastructure/, helpers/)
46
+ db/migrations/ PostgreSQL schema (profile-specific prefixes)
47
+ index.js public API facade
48
+ ```
49
+
50
+ ## Three layers
51
+
52
+ | Layer | Role | Examples |
53
+ |-------|------|----------|
54
+ | **domain/** | Pure business logic, no imports from infrastructure | `plant`, `shop`, `machine`, `timeline`, `alerts`, `segment/dispatch` |
55
+ | **application/** | Wires domain + infrastructure into runnable units | `plantApi`, `metricsPlant`, `supervisorSink`, `siteServer` |
56
+ | **infrastructure/** | I/O adapters behind narrow ports | `persistence/pg`, `http/plant`, `http/edge`, `messaging/stomp`, `ingest` |
57
+
58
+ Dependency rules:
59
+
60
+ - `domain/` imports `domain/` only
61
+ - `application/` composes `domain/` + `infrastructure/`
62
+ - HTTP routes receive injected `plant` objects; storage backends are swappable behind the same port shape
63
+
64
+ ## Data flow
65
+
66
+ **Central plant API** (read path):
67
+
68
+ ```
69
+ ClickHouse ← mqttMetrics / amqpMetricsIngest
70
+ PostgreSQL ← STOMP alerts (via plantServer)
71
+ plantApi → REST + SSE → frontend / @yarkivaev/scada/client
72
+ ```
73
+
74
+ **Supervisor-sink** (write path):
75
+
76
+ ```
77
+ STOMP (`/queue/scada.segments.ingest`, `/exchange/scada.alerts`, `/exchange/scada.user_decisions`)
78
+ → ingest/codecs → domain/segment → ingest/sinks → PostgreSQL
79
+ edgeApi (:8081) ← metrics batch, checkpoints, retention (edge profile)
80
+ ```
81
+
82
+ **Edge site** (`siteServer`): runs supervisor-sink HTTP, plant API, and optional MQTT ingest in one process. Metrics land in PostgreSQL on edge; on central they go to ClickHouse.
83
+
84
+ ## Public exports
85
+
86
+ Main entry (`import { … } from '@yarkivaev/scada'`):
87
+
88
+ | Export | Purpose |
89
+ |--------|---------|
90
+ | `plant`, `shop`, `machine` | Domain hierarchy |
91
+ | `timeline`, `alerts`, `alert`, `acknowledgedAlert` | Timeline and alerting |
92
+ | `plantApi`, `plantServer`, `siteServer` | HTTP composition |
93
+ | `exportQuery`, `exportStream`, `exportSink`, `exportJob` | Generic export ports over plantApi / `@yarkivaev/scada/client` |
94
+ | `metricsPlant`, `shopWithTimeline`, `machineInPlant` | Plant wiring helpers |
95
+ | `supervisorSink`, `readDeploymentConfig` | STOMP ingest + PG persistence |
96
+ | `edgeApi`, `stateHttpClient`, `metricsSensor` | Edge HTTP read/write |
97
+ | `clickhouseConnection`, `clickhouseSensor`, `pgMetricsSensor` | Metric storage |
98
+ | `pgTimeline`, `pgAlerts`, `stateDataFromPool`, `stateDataFromMemory` | State ports |
99
+ | `stompTimeline`, `stompAlerts`, `userDecisions` | STOMP messaging |
100
+ | `machineOwners`, `ownerTimeline`, `httpTimeline`, `httpOperations` | Owner-routed timeline and operations write (local or edge HTTP) |
101
+ | `mqttMetrics`, `modbusMqtt`, `activityTracking`, `amqpMetricsIngest` | Ingest pipelines |
102
+
103
+ Subpath exports:
104
+
105
+ - `@yarkivaev/scada/client` — browser/Node HTTP+SSE client (`scadaClient`, `machineClient`) plus re-exported export ports
106
+ - `@yarkivaev/scada/stateDataFake` — in-memory state backends for tests
107
+ - `@yarkivaev/scada/startTestEdgeApi` — ephemeral edge API for integration tests
108
+
109
+ ## Export ports
110
+
111
+ Generic read/stream/write contract for outbound jobs. Read side uses existing `plantApi` / `@yarkivaev/scada/client` only (no direct DB). Destination adapters and plant maps live in plant packages, not here. Do not put export into `edgeApi`.
112
+
113
+ ```javascript
114
+ import { exportQuery, exportStream, exportSink, exportJob } from '@yarkivaev/scada';
115
+ import { scadaClient } from '@yarkivaev/scada/client';
116
+
117
+ const client = scadaClient(baseUrl, fetch, EventSource);
118
+ const query = exportQuery(client);
119
+ const stream = exportStream(client);
120
+ const sink = exportSink({
121
+ write(records) { return destination.write(records); },
122
+ send(artifact) { return destination.send(artifact); }
123
+ });
124
+ const job = exportJob({ query, transform: (rows) => rows, sink });
125
+ await job.run({ kind: 'segments', machine: 'furnace-α', from, to });
126
+ ```
127
+
128
+ Downstream plant packages should pin a released tag (e.g. `#v2.3.45`).
129
+
130
+ Alert and HMI copy are inject-only: pass `translations` into `siteServer` / `plantServer` / `alertPipeline` config, and override `TAG_CATALOG_PATH` or `plantApi({ tagCatalog })` for site taxonomy. Defaults ship empty or English stubs.
131
+
132
+ ## Binaries
133
+
134
+ | Binary | Role |
135
+ |--------|------|
136
+ | `supervisor-sink` | STOMP → PostgreSQL ingest + edge HTTP API (`STATE_HTTP_PORT`, default 8081) |
137
+
138
+ Full plant API with site-specific routes: plant packages call `siteServer` from this package.
139
+
140
+ ## Sensor port
141
+
142
+ All sensor implementations expose the same interface:
12
143
 
13
144
  ```javascript
14
- import { plant, meltingShop, meltingMachine, meltings, alerts, initialized } from 'scada';
145
+ { name(), current(), measurements(range, step), stream(since, step, callback, clock?) }
146
+ ```
147
+
148
+ Implementations: `clickhouseSensor` (central; live `stream()` batches many topics into one ClickHouse query per tick), `pgMetricsSensor` / `metricsSensor` (edge HTTP or PG).
149
+
150
+ ## Database migrations
151
+
152
+ SQL files in `db/migrations/` use profile prefixes:
153
+
154
+ | Prefix | Applies to |
155
+ |--------|------------|
156
+ | `V*` | Both central and edge |
157
+ | `E*` | Edge only (metrics tables, retention) |
158
+ | `C*` | Central only (revoke delete on metrics) |
159
+
160
+ Set `SINK_DB_PROFILE=central|edge`. Migrations run at supervisor-sink startup via `POSTGRES_ADMIN_URL`.
15
161
 
16
- const machine = meltingMachine('icht1', sensors, alertHistory);
17
- const shop = meltingShop('shop1', initialized({ icht1: machine }, Object.values), meltings());
18
- const factory = plant(initialized({ shop1: shop }, Object.values));
162
+ ## Environment (supervisor-sink / siteServer)
163
+
164
+ | Variable | Purpose |
165
+ |----------|---------|
166
+ | `STOMP_URL` | RabbitMQ STOMP broker |
167
+ | `POSTGRES_URL` | Application DB user (`supervisor_sink`) |
168
+ | `POSTGRES_ADMIN_URL` | Admin user for migrations |
169
+ | `SINK_DB_PROFILE` | `central` or `edge` |
170
+ | `STATE_HTTP_PORT` / `STATE_HTTP_TOKEN` | Edge internal API |
171
+ | `MQTT_URL`, `MQTT_TOPICS` | Optional metrics ingest |
172
+ | `CLICKHOUSE_URL` | Central metrics sink |
173
+ | `PORT` | Plant API port (siteServer, default 3000) |
174
+
175
+ ## Testing
176
+
177
+ ```bash
178
+ npm test # all Mocha unit tests
179
+ npm test -- --grep "pattern" # filter by name
180
+ npm run test:deep # domain + ingest (integration needs Docker)
181
+ npm run coverage
182
+ npm run lint
183
+ npm run lint:eo # Elegant Objects naming audit
19
184
  ```
20
185
 
21
- ## Modules
186
+ Tests mirror `src/` under `test/`. Use fakes instead of mocks; one assertion per test. Integration tests under `test/infrastructure/**/integration/` use testcontainers.
187
+
188
+ For HTTP/state tests without PostgreSQL:
189
+
190
+ ```javascript
191
+ import stateDataFake from '@yarkivaev/scada/stateDataFake';
192
+ ```
22
193
 
23
- ### Plant Hierarchy
24
- - `plant`
25
- - `meltingShop`
26
- - `meltingMachine`
27
- - `machineChronology`
28
- - `monitoredMeltingMachine`
194
+ ## Design conventions
29
195
 
30
- ### Melting Operations
31
- - `activeMelting`
32
- - `completedMelting`
33
- - `meltings`
34
- - `meltingChronology`
196
+ - Factory functions returning frozen objects — no classes, no inheritance
197
+ - ESLint limits: `max-lines: 200`, `max-lines-per-function: 50`, `no-console`, `no-inline-comments`
198
+ - Docblocks on every module; method bodies without blank lines
199
+ - See `CLAUDE.md` for layer rules and ingest sandwich diagram
35
200
 
36
- ### Alerting
37
- - `alert`
38
- - `acknowledgedAlert`
39
- - `alerts`
40
- - `meltingRuleEngine`
201
+ ## Package boundary
41
202
 
42
- ### Sensors
43
- - `scyllaSensor`
44
- - `clickhouseSensor`
203
+ | This package (`@yarkivaev/scada`) | Downstream plant package |
204
+ |------------------------|--------------------------|
205
+ | Generic plant, timeline, alerts, ingest | Site domain, sensors, HMI strings, tag taxonomy |
206
+ | Empty/injectable alert `translations` | Localized rule messages |
207
+ | Opaque operation `kind` strings | Site-specific kinds (`sample`, `load`, …) |
208
+ | `plantApi`, `siteServer`, `supervisorSink` | Site bins and routes |
209
+ | Export ports `Query` / `Stream` / `Sink` / `Job` | Export jobs + destination adapters |
210
+ | ClickHouse + PG adapters | Plant wiring (`metricsPlant`, shops, machines) |
45
211
 
46
- ### Utilities
47
- - `interval`
48
- - `initialized`
49
- - `events`
212
+ Depend on a pinned version (`"@yarkivaev/scada": "…#v2.3.45"`), not a local path, in downstream packages.
50
213
 
51
- ## License
214
+ ## CI/CD
52
215
 
53
- MIT
216
+ GitHub Actions via `yarkivaev/npm-workflows` (lint, test, version check). This repo publishes an npm package only; container images are built in downstream deploy repos.
@@ -0,0 +1 @@
1
+ DROP TABLE IF EXISTS metrics;
@@ -0,0 +1,12 @@
1
+ REVOKE ALL ON SCHEMA public FROM supervisor_sink;
2
+ GRANT USAGE ON SCHEMA public TO supervisor_sink;
3
+
4
+ GRANT SELECT, INSERT, UPDATE ON segments TO supervisor_sink;
5
+ GRANT SELECT, INSERT, UPDATE ON alerts TO supervisor_sink;
6
+ GRANT SELECT, INSERT ON user_decisions TO supervisor_sink;
7
+ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO supervisor_sink;
8
+
9
+ REVOKE CREATE ON SCHEMA public FROM supervisor_sink;
10
+ REVOKE DELETE ON segments FROM supervisor_sink;
11
+ REVOKE DELETE ON alerts FROM supervisor_sink;
12
+ REVOKE DELETE ON user_decisions FROM supervisor_sink;
@@ -0,0 +1,7 @@
1
+ CREATE TABLE IF NOT EXISTS operators (
2
+ id SERIAL PRIMARY KEY,
3
+ card_uid TEXT NOT NULL UNIQUE,
4
+ first_name TEXT NOT NULL,
5
+ last_name TEXT NOT NULL,
6
+ display_name TEXT NOT NULL
7
+ );
@@ -0,0 +1,2 @@
1
+ GRANT SELECT, INSERT, UPDATE ON operations TO supervisor_sink;
2
+ GRANT SELECT, INSERT, UPDATE ON ingest_checkpoints TO supervisor_sink;
@@ -0,0 +1 @@
1
+ GRANT SELECT ON operators TO supervisor_sink;
@@ -0,0 +1,16 @@
1
+ -- Singleton flag: HMI may register operators only while enabled is true.
2
+ -- CHECK keeps exactly one row (singleton = 1); default disabled until console turns registration on.
3
+ CREATE TABLE IF NOT EXISTS operators_registration (
4
+ singleton SMALLINT PRIMARY KEY DEFAULT 1 CHECK (singleton = 1),
5
+ enabled BOOLEAN NOT NULL DEFAULT FALSE
6
+ );
7
+
8
+ -- Seed the singleton on first apply; keep an existing enabled value on re-apply.
9
+ INSERT INTO operators_registration (singleton, enabled)
10
+ VALUES (1, FALSE)
11
+ ON CONFLICT (singleton) DO NOTHING;
12
+
13
+ -- Plant API role: create operators and read/update the registration flag.
14
+ GRANT INSERT ON operators TO supervisor_sink;
15
+ GRANT USAGE, SELECT ON SEQUENCE operators_id_seq TO supervisor_sink;
16
+ GRANT SELECT, UPDATE ON operators_registration TO supervisor_sink;
@@ -0,0 +1,8 @@
1
+ CREATE TABLE IF NOT EXISTS metrics (
2
+ topic TEXT NOT NULL,
3
+ ts TIMESTAMPTZ NOT NULL,
4
+ value DOUBLE PRECISION NOT NULL
5
+ );
6
+
7
+ CREATE INDEX IF NOT EXISTS idx_metrics_topic_ts
8
+ ON metrics (topic, ts);
@@ -0,0 +1,8 @@
1
+ ALTER TABLE IF EXISTS metrics OWNER TO supervisor_sink;
2
+
3
+ GRANT SELECT, INSERT, UPDATE, DELETE ON metrics TO supervisor_sink;
4
+ GRANT DELETE ON segments TO supervisor_sink;
5
+ GRANT DELETE ON alerts TO supervisor_sink;
6
+ GRANT DELETE ON user_decisions TO supervisor_sink;
7
+
8
+ ALTER TABLE IF EXISTS user_decisions OWNER TO supervisor_sink;
@@ -0,0 +1,2 @@
1
+ GRANT SELECT, INSERT, UPDATE ON operations TO supervisor_sink;
2
+ GRANT SELECT, INSERT, UPDATE ON ingest_checkpoints TO supervisor_sink;
@@ -0,0 +1,25 @@
1
+ CREATE TABLE IF NOT EXISTS segments (
2
+ machine TEXT,
3
+ name TEXT,
4
+ start_time TIMESTAMPTZ,
5
+ end_time TIMESTAMPTZ,
6
+ duration FLOAT,
7
+ options TEXT,
8
+ tags TEXT,
9
+ properties TEXT,
10
+ resolved BOOLEAN DEFAULT TRUE,
11
+ consumed BOOLEAN DEFAULT TRUE
12
+ );
13
+
14
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_segments_machine_start
15
+ ON segments (machine, start_time);
16
+
17
+ CREATE TABLE IF NOT EXISTS alerts (
18
+ id SERIAL PRIMARY KEY,
19
+ name TEXT,
20
+ message TEXT,
21
+ machine TEXT,
22
+ severity TEXT,
23
+ timestamp TIMESTAMPTZ,
24
+ acknowledged BOOLEAN DEFAULT FALSE
25
+ );
@@ -0,0 +1,6 @@
1
+ CREATE TABLE IF NOT EXISTS user_decisions (
2
+ machine TEXT,
3
+ start_time TIMESTAMPTZ,
4
+ username TEXT,
5
+ payload TEXT
6
+ );
@@ -0,0 +1,10 @@
1
+ CREATE TABLE IF NOT EXISTS operations (
2
+ machine TEXT NOT NULL,
3
+ occurred_at TIMESTAMPTZ NOT NULL,
4
+ kind TEXT NOT NULL,
5
+ key TEXT NOT NULL,
6
+ payload JSONB NOT NULL
7
+ );
8
+
9
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_operations_key
10
+ ON operations (key);
@@ -0,0 +1,6 @@
1
+ CREATE TABLE IF NOT EXISTS ingest_checkpoints (
2
+ source TEXT NOT NULL,
3
+ machine TEXT NOT NULL,
4
+ cursor_at TIMESTAMPTZ NOT NULL,
5
+ PRIMARY KEY (source, machine)
6
+ );
@@ -0,0 +1,2 @@
1
+ ALTER TABLE user_decisions ADD COLUMN IF NOT EXISTS operator_id INTEGER;
2
+ ALTER TABLE user_decisions ADD COLUMN IF NOT EXISTS decided_at TIMESTAMPTZ;
package/index.js CHANGED
@@ -1,51 +1,80 @@
1
1
  /**
2
- * SCADA domain objects for industrial plant monitoring.
3
- * Provides immutable objects for modeling melting operations,
4
- * alerting systems, events, and plant hierarchy.
5
- *
6
- * @example
7
- * import { plant, meltingShop, meltingMachine, meltings, alerts, initialized } from 'scada';
8
- * const shop = meltingShop('shop1', initialized({ m1: machine }, Object.values), meltings());
9
- * const p = plant(initialized({ shop1: shop }, Object.values));
2
+ * scada v2 generic industrial monitoring domain and HTTP API.
10
3
  */
11
4
 
12
- // Plant hierarchy
13
- export { default as plant } from './src/plant.js';
14
- export { default as meltingShop } from './src/meltingShop.js';
15
- export { default as meltingMachine } from './src/meltingMachine.js';
16
- export { default as machineChronology } from './src/machineChronology.js';
17
- export { default as monitoredMeltingMachine } from './src/monitoredMeltingMachine.js';
5
+ export { default as plant } from './src/domain/plant/plant.js';
6
+ export { default as shop } from './src/domain/plant/shop.js';
7
+ export { default as machine } from './src/domain/plant/machine.js';
8
+ export { default as timeline } from './src/domain/timeline/timeline.js';
9
+ export { alert, acknowledgedAlert } from './src/domain/alerting/alert.js';
10
+ export { operation } from './src/domain/operation/operation.js';
11
+ export { ingestCursorAt, ingestCursorEmpty } from './src/domain/ingest/ingestCursor.js';
12
+ export { default as alerts } from './src/domain/alerting/alerts.js';
13
+ export { default as initialized } from './src/domain/shared/initialized.js';
14
+ export { default as pubsub } from './src/domain/shared/pubsub.js';
18
15
 
19
- // Melting operations
20
- export { default as activeMelting } from './src/activeMelting.js';
21
- export { default as completedMelting } from './src/completedMelting.js';
22
- export { default as meltings } from './src/meltings.js';
23
- export { default as meltingChronology } from './src/meltingChronology.js';
16
+ export { default as plantApi } from './src/application/plantApi.js';
17
+ export { default as tagCatalog } from './src/infrastructure/catalog/tagCatalog.js';
18
+ export { default as machineInPlant } from './src/application/machineInPlant.js';
19
+ export { default as shopWithTimeline } from './src/application/shopWithTimeline.js';
20
+ export { default as plantOperations } from './src/application/plantOperations.js';
21
+ export { default as metricsPlant } from './src/application/metricsPlant.js';
22
+ export { default as plantServer } from './src/application/plantServer.js';
23
+ export { default as siteServer } from './src/application/siteServer.js';
24
+ export {
25
+ default as siteOperatorCatalog,
26
+ buildSiteOperatorCatalog
27
+ } from './src/application/siteOperatorCatalog.js';
28
+ export { default as edgeOperatorCatalog } from './src/application/edgeOperatorCatalog.js';
29
+ export { default as operator } from './src/domain/operator/operator.js';
30
+ export { default as operatorsFromPg } from './src/infrastructure/persistence/pg/operators.js';
31
+ export { default as userDecisionsFromPg } from './src/infrastructure/persistence/pg/userDecisions.js';
32
+ export { default as operatorRoute } from './src/infrastructure/http/plant/routes/operatorRoute.js';
33
+ export { default as decisionRoute } from './src/infrastructure/http/plant/routes/decisionRoute.js';
34
+ export { default as operatorJson } from './src/infrastructure/http/plant/json/operatorJson.js';
35
+ export { default as operators } from './src/infrastructure/operators/operators.js';
36
+ export { default as centralOperators } from './src/infrastructure/operators/centralOperators.js';
37
+ export { default as edgeOperators } from './src/infrastructure/operators/edgeOperators.js';
38
+ export { default as operatorsSync } from './src/infrastructure/operators/operatorsSync.js';
39
+ export { default as operatorExtras } from './src/infrastructure/operators/operatorExtras.js';
24
40
 
25
- // Events system
26
- export { default as event } from './src/event.js';
27
- export { default as events } from './src/events.js';
28
- export { default as rule } from './src/rule.js';
29
- export { default as rules } from './src/rules.js';
30
-
31
- // Rule engine (legacy)
32
- export { default as meltingRuleEngine } from './src/meltingRuleEngine.js';
33
-
34
- // Alerting
35
- export { alert, acknowledgedAlert } from './src/alert.js';
36
- export { default as alerts } from './src/alerts.js';
37
-
38
- // Timeline
39
- export { default as segments } from './src/segments.js';
40
- export { default as requests } from './src/requests.js';
41
-
42
- // Utilities
43
- export { default as interval } from './src/interval.js';
44
- export { default as initialized } from './src/initialized.js';
45
- export { default as pubsub } from './src/pubsub.js';
46
-
47
- // Sensors
48
- export { default as scyllaSensor } from './src/scyllaSensor.js';
49
- export { default as clickhouseSensor } from './src/clickhouseSensor.js';
50
- export { default as sqliteSensor } from './src/sqliteSensor.js';
51
- export { default as postgresSensor } from './src/postgresSensor.js';
41
+ export { default as pgTimeline } from './src/infrastructure/persistence/pg/timeline.js';
42
+ export { default as pgAlerts } from './src/infrastructure/persistence/pg/alerts.js';
43
+ export { default as pgOperations } from './src/infrastructure/persistence/pg/operations.js';
44
+ export { default as clickhouseConnection } from './src/infrastructure/persistence/clickhouse/connection.js';
45
+ export { default as clickhouseSensor } from './src/infrastructure/persistence/clickhouse/sensor.js';
46
+ export { default as postgresPool } from './src/infrastructure/persistence/postgresPool.js';
47
+ export { metricsSinkFromPool as pgMetricsSink } from './src/infrastructure/persistence/pg/metrics.js';
48
+ export { pgMetricsSensor } from './src/infrastructure/persistence/metricsSensor.js';
49
+ export { default as stompTimeline } from './src/infrastructure/messaging/stomp/timeline.js';
50
+ export { default as stompTimelineSegments } from './src/infrastructure/messaging/stomp/stompTimelineSegments.js';
51
+ export { default as machineOwners } from './src/infrastructure/messaging/ownership/machineOwners.js';
52
+ export { default as httpTimeline } from './src/infrastructure/messaging/ownership/httpTimeline.js';
53
+ export { default as httpOperations } from './src/infrastructure/messaging/ownership/httpOperations.js';
54
+ export { default as ownerTimeline } from './src/infrastructure/messaging/ownership/ownerTimeline.js';
55
+ export { default as stateDataFromPool } from './src/infrastructure/persistence/stateDataFromPool.js';
56
+ export { default as stateDataFromMemory } from './src/infrastructure/persistence/stateDataFromMemory.js';
57
+ export { default as stateDataFake } from './src/infrastructure/persistence/stateDataFromMemory.js';
58
+ export { default as supervisorSink, readDeploymentConfig } from './src/application/supervisorSink.js';
59
+ export { default as runRetention } from './src/infrastructure/ingest/db/runRetention.js';
60
+ export { createEdgeApi, default as edgeApi } from './src/infrastructure/http/edge/edgeApi.js';
61
+ export { default as startTestEdgeApi } from './src/infrastructure/http/edge/startTestEdgeApi.js';
62
+ export { default as stateHttpClient } from './src/infrastructure/http/edge/stateHttpClient.js';
63
+ export { default as metricsSensor } from './src/infrastructure/http/edge/metricsSensor.js';
64
+ export { default as userDecisions } from './src/infrastructure/messaging/stomp/userDecisions.js';
65
+ export { default as stompAlerts } from './src/infrastructure/messaging/stomp/alerts/stompAlerts.js';
66
+ export { default as stompAlertsInit } from './src/infrastructure/messaging/stomp/alerts/stompAlertsInit.js';
67
+ export { default as mqttMetrics } from './src/infrastructure/ingest/mqtt/mqttMetrics.js';
68
+ export { default as modbusMqtt } from './src/infrastructure/ingest/mqtt/modbusMqtt.js';
69
+ export { default as mx210Tcp } from './src/infrastructure/ingest/modbus/mx210Tcp.js';
70
+ export { default as silentStreams } from './src/infrastructure/ingest/modbus/silentStreams.js';
71
+ export { default as activityTracking } from './src/infrastructure/ingest/activity/activityTracking.js';
72
+ export { default as amqpMetricsIngest } from './src/infrastructure/ingest/telemetry/amqpMetricsIngest.js';
73
+ export { default as amqpMqttRelay } from './src/infrastructure/ingest/telemetry/amqpMqttRelay.js';
74
+ export { default as operationSyncIngest } from './src/infrastructure/sync/operationSyncIngest.js';
75
+ export { default as ingestCheckpointMemory, ingestCheckpointPg } from './src/infrastructure/ingest/ingestCheckpoint.js';
76
+ export { default as bindSilentStreams } from './src/application/bindSilentStreams.js';
77
+ export { default as exportQuery } from './src/application/export/exportQuery.js';
78
+ export { default as exportStream } from './src/application/export/exportStream.js';
79
+ export { default as exportSink } from './src/application/export/exportSink.js';
80
+ export { default as exportJob } from './src/application/export/exportJob.js';
package/package.json CHANGED
@@ -1,33 +1,52 @@
1
1
  {
2
2
  "name": "@yarkivaev/scada",
3
- "version": "1.5.0",
4
- "description": "SCADA domain objects for plant monitoring",
3
+ "version": "2.3.45",
4
+ "description": "SCADA domain objects, state persistence, and plant monitoring",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/yarkivaev/scada"
8
8
  },
9
9
  "type": "module",
10
10
  "main": "index.js",
11
+ "exports": {
12
+ ".": "./index.js",
13
+ "./client": "./src/infrastructure/client/index.js",
14
+ "./stateDataFake": "./src/infrastructure/persistence/stateDataFromMemory.js",
15
+ "./startTestEdgeApi": "./src/infrastructure/http/edge/startTestEdgeApi.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "bin": {
19
+ "supervisor-sink": "./src/bin/supervisor-sink.js"
20
+ },
11
21
  "scripts": {
12
- "test": "mocha --exit",
22
+ "test": "mocha --exit 'test/**/test*.js'",
23
+ "test:deep": "mocha --exit 'test/domain/**/test*.js' 'test/infrastructure/ingest/**/test*.js'",
13
24
  "coverage": "c8 --all --src src npm test",
14
- "lint": "eslint .",
15
- "lint:fix": "eslint . --fix"
25
+ "lint": "eslint . && node scripts/check-eo-names.mjs",
26
+ "lint:fix": "eslint . --fix",
27
+ "lint:eo": "node scripts/check-eo-names.mjs"
16
28
  },
17
- "devDependencies": {
29
+ "dependencies": {
18
30
  "@clickhouse/client": "^1.0.0",
31
+ "@yarkivaev/simple-server": "^1.0.0",
32
+ "@yarkivaev/source-to-sink": "1.3.2",
33
+ "amqplib": "^0.10.9",
34
+ "pg": "^8.0.0",
35
+ "stompit": "^1.0.0"
36
+ },
37
+ "devDependencies": {
19
38
  "@eslint/js": "^9.39.2",
39
+ "better-sqlite3": "^11.0.0",
20
40
  "c8": "^10.1.3",
21
41
  "cassandra-driver": "^4.7.2",
22
42
  "eslint": "^9.39.2",
23
43
  "globals": "^17.0.0",
24
- "mocha": "^10.2.0",
25
- "better-sqlite3": "^11.0.0",
26
- "pg": "^8.0.0",
44
+ "mocha": "^10.8.2",
27
45
  "testcontainers": "^10.13.0"
28
46
  },
29
47
  "files": [
30
48
  "index.js",
49
+ "db/",
31
50
  "src/"
32
51
  ]
33
52
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Binds silent Modbus streams to a metrics sink and returns an onSeen callback.
3
+ *
4
+ * @example
5
+ * const onSeen = bindSilentStreams(streams, clickhouseSink);
6
+ * amqpMetricsIngest(url, queue, sink, { onSeen });
7
+ *
8
+ * @param {object|undefined} streams - silentStreams gate or omitted
9
+ * @param {object} sink - metrics sink with write(records)
10
+ * @returns {Function|undefined} onSeen(name) or undefined
11
+ */
12
+ export default function bindSilentStreams(streams, sink) {
13
+ if (!streams) {
14
+ return undefined;
15
+ }
16
+ streams.bind(sink);
17
+ return (name) => {
18
+ streams.seen(name);
19
+ };
20
+ }
@@ -0,0 +1,45 @@
1
+ import operators from '../infrastructure/operators/operators.js';
2
+ import centralOperators from '../infrastructure/operators/centralOperators.js';
3
+ import operatorsSync from '../infrastructure/operators/operatorsSync.js';
4
+ import edgeOperators from '../infrastructure/operators/edgeOperators.js';
5
+ import stateHttpClient from '../infrastructure/http/edge/stateHttpClient.js';
6
+ import operatorRoute from '../infrastructure/http/plant/routes/operatorRoute.js';
7
+
8
+ const DEFAULT_SYNC_INTERVAL_SEC = 30;
9
+
10
+ function syncIntervalMs(env) {
11
+ const sec = parseInt(env.OPERATORS_SYNC_INTERVAL_SEC || String(DEFAULT_SYNC_INTERVAL_SEC), 10);
12
+ if (!Number.isFinite(sec) || sec <= 0) {
13
+ return DEFAULT_SYNC_INTERVAL_SEC * 1000;
14
+ }
15
+ return sec * 1000;
16
+ }
17
+
18
+ /**
19
+ * Edge operator catalog: in-memory cache, periodic central sync, plant routes.
20
+ * POST create proxies to central and refreshes cache; registration flag is read from cache.
21
+ * Without CENTRAL_PLANT_URL create fails fast; first boot yields an empty list until sync.
22
+ *
23
+ * @param {string} basePath - plant API base path
24
+ * @param {object} env - process environment
25
+ * @returns {object} routes array and optional sync job
26
+ *
27
+ * @example
28
+ * edgeOperatorCatalog('/api/v1', process.env);
29
+ */
30
+ export default function edgeOperatorCatalog(basePath, env) {
31
+ const cache = operators();
32
+ const centralUrl = env.CENTRAL_PLANT_URL;
33
+ if (!centralUrl) {
34
+ const provider = edgeOperators(cache, undefined);
35
+ return { routes: operatorRoute(basePath, provider), sync: undefined, provider };
36
+ }
37
+ const client = stateHttpClient({
38
+ baseUrl: centralUrl,
39
+ token: env.CENTRAL_PLANT_TOKEN
40
+ });
41
+ const source = centralOperators(client, basePath);
42
+ const provider = edgeOperators(cache, source);
43
+ const sync = operatorsSync(source, cache, { intervalMs: syncIntervalMs(env) });
44
+ return { routes: operatorRoute(basePath, provider), sync, provider };
45
+ }