@yarkivaev/scada 1.5.0 → 2.3.46

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 +79 -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
@@ -0,0 +1,191 @@
1
+ import { clickhouseSink } from '@yarkivaev/source-to-sink';
2
+ import supervisorSink from './supervisorSink.js';
3
+ import plantServer from './plantServer.js';
4
+ import plantOperations from './plantOperations.js';
5
+ import edgeApi from '../infrastructure/http/edge/edgeApi.js';
6
+ import runRetention from '../infrastructure/ingest/db/runRetention.js';
7
+ import mqttMetrics from '../infrastructure/ingest/mqtt/mqttMetrics.js';
8
+ import amqpMetricsIngest from '../infrastructure/ingest/telemetry/amqpMetricsIngest.js';
9
+ import operationSyncIngest from '../infrastructure/sync/operationSyncIngest.js';
10
+ import { metricsSinkFromPool } from '../infrastructure/persistence/pg/metrics.js';
11
+ import bindSilentStreams from './bindSilentStreams.js';
12
+ import timelineOperatorFromEnv from './timelineOperatorFromEnv.js';
13
+ import { buildSiteOperatorCatalog } from './siteOperatorCatalog.js';
14
+
15
+ function stompFromEnv(env) {
16
+ return {
17
+ url: env.STOMP_URL,
18
+ login: env.STOMP_LOGIN || 'guest',
19
+ passcode: env.STOMP_PASSCODE || 'guest',
20
+ host: env.STOMP_HOST || '/',
21
+ operatorUser: env.HMI_OPERATOR_USER || 'hmi-kiosk'
22
+ };
23
+ }
24
+
25
+ function mqttConfigFromEnv(env) {
26
+ return {
27
+ size: parseInt(env.BATCH_SIZE || '50', 10),
28
+ interval: parseInt(env.FLUSH_INTERVAL || '5', 10),
29
+ threshold: parseInt(env.CIRCUIT_THRESHOLD || '5', 10),
30
+ timeout: parseInt(env.CIRCUIT_TIMEOUT || '60', 10),
31
+ clientId: env.CLIENT_ID,
32
+ sessionExpiryInterval: parseInt(env.SESSION_EXPIRY || '3600', 10)
33
+ };
34
+ }
35
+
36
+ function mqttSinkForProfile(sink, env) {
37
+ const profile = env.SINK_DB_PROFILE === 'central' ? 'central' : 'edge';
38
+ if (profile === 'edge') {
39
+ return metricsSinkFromPool(sink.pool);
40
+ }
41
+ const url = env.CLICKHOUSE_URL
42
+ || (env.CLICKHOUSE_HOST ? `http://${env.CLICKHOUSE_HOST}:8123` : undefined);
43
+ if (!url) {
44
+ throw new Error('CLICKHOUSE_URL or CLICKHOUSE_HOST is required for central MQTT metrics');
45
+ }
46
+ return clickhouseSink(url, 'scada.metrics');
47
+ }
48
+
49
+ function startMqtt(sink, env) {
50
+ if (!env.MQTT_URL || !env.MQTT_TOPICS) {
51
+ return undefined;
52
+ }
53
+ const metricsSink = mqttSinkForProfile(sink, env);
54
+ const pipeline = mqttMetrics(env.MQTT_URL, metricsSink, env.MQTT_TOPICS, mqttConfigFromEnv(env));
55
+ pipeline.start();
56
+ return pipeline;
57
+ }
58
+
59
+ function startOperationSync(sink, env) {
60
+ if (env.SINK_DB_PROFILE === 'edge' || !env.AMQP_URL) {
61
+ return undefined;
62
+ }
63
+ const ingest = operationSyncIngest(
64
+ env.AMQP_URL,
65
+ env.AMQP_OPERATIONS_QUEUE || 'scada.operations.ingest',
66
+ sink.dataAccess.operations,
67
+ { prefetch: parseInt(env.AMQP_PREFETCH || '32', 10) }
68
+ );
69
+ ingest.start();
70
+ return ingest;
71
+ }
72
+
73
+ function clickhouseMetricsUrl(env) {
74
+ return env.CLICKHOUSE_URL
75
+ || (env.CLICKHOUSE_HOST ? `http://${env.CLICKHOUSE_HOST}:8123` : undefined);
76
+ }
77
+
78
+ function startAmqpMetrics(env, sink, onSeen) {
79
+ if (!env.AMQP_URL) {
80
+ return undefined;
81
+ }
82
+ const batchConfig = mqttConfigFromEnv(env);
83
+ const ingest = amqpMetricsIngest(
84
+ env.AMQP_URL,
85
+ env.AMQP_QUEUE || 'scada.telemetry.ingest',
86
+ sink,
87
+ {
88
+ size: batchConfig.size,
89
+ interval: batchConfig.interval,
90
+ threshold: batchConfig.threshold,
91
+ timeout: batchConfig.timeout,
92
+ prefetch: parseInt(env.AMQP_PREFETCH || '32', 10),
93
+ onSeen
94
+ }
95
+ );
96
+ ingest.start();
97
+ return ingest;
98
+ }
99
+
100
+ function startTelemetryIngest(env, streams) {
101
+ if (env.SINK_DB_PROFILE === 'edge' || (!env.AMQP_URL && !streams)) {
102
+ return undefined;
103
+ }
104
+ const url = clickhouseMetricsUrl(env);
105
+ if (!url) {
106
+ throw new Error('CLICKHOUSE_URL or CLICKHOUSE_HOST is required for AMQP telemetry ingest');
107
+ }
108
+ const sink = clickhouseSink(url, 'scada.metrics');
109
+ const onSeen = bindSilentStreams(streams, sink);
110
+ const ingest = startAmqpMetrics(env, sink, onSeen);
111
+ if (streams) {
112
+ streams.start();
113
+ }
114
+ return { ingest, streams };
115
+ }
116
+
117
+ function plantFactoryWithOperations(plantFactory, ops, sink) {
118
+ return (ctx) => {
119
+ const built = plantFactory({ ...ctx, operations: ops }, sink);
120
+ return Promise.resolve(built).then((p) => {
121
+ if (p.operations) {
122
+ return p;
123
+ }
124
+ return {
125
+ ...p,
126
+ operations: ops,
127
+ init() {
128
+ p.init();
129
+ }
130
+ };
131
+ });
132
+ };
133
+ }
134
+
135
+ function siteExtraRoutes(catalog, extraRoutes) {
136
+ return (path, plant, clock) => {
137
+ const userExtra = extraRoutes ? extraRoutes(path, plant, clock) : [];
138
+ return [...catalog.routes, ...userExtra];
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Unified site process: supervisor-sink HTTP, plant API, and optional MQTT ingest.
144
+ *
145
+ * @param {object} config - port, basePath, translations, requirePool, plantFactory, extraRoutes, operatorCatalog, kindSources, streams, env
146
+ * @returns {Promise<object>} sink, plant, mqtt pipeline
147
+ *
148
+ * @example
149
+ * await siteServer({ plantFactory: ({ alerts, userDecisions }, sink) => edgePlant(machines, { metrics: sink.dataAccess.metrics, alerts, userDecisions }), kindSources: { temp } });
150
+ */
151
+ export default async function siteServer(config) {
152
+ const env = config.env || process.env;
153
+ const sink = supervisorSink(env);
154
+ const ops = plantOperations(
155
+ sink.dataAccess.operations,
156
+ config.kindSources || config.operationSources
157
+ );
158
+ sink.dataAccess.operations = ops;
159
+ const http = edgeApi(sink.dataAccess, {
160
+ port: sink.apiPort,
161
+ token: sink.apiToken,
162
+ metricsEnabled: sink.metricsEnabled,
163
+ retentionEnabled: sink.retentionEnabled,
164
+ retentionDays: sink.retentionDays,
165
+ requestTimeoutMs: sink.requestTimeoutMs,
166
+ pool: sink.pool,
167
+ runRetention
168
+ });
169
+ await sink.run(http);
170
+ const mqtt = startMqtt(sink, env);
171
+ const telemetry = startTelemetryIngest(env, config.streams);
172
+ const operationSync = startOperationSync(sink, env);
173
+ const basePath = config.basePath || '/api/v1';
174
+ const catalog = buildSiteOperatorCatalog(config, basePath, sink, env);
175
+ if (catalog.sync) {
176
+ await catalog.sync.start();
177
+ }
178
+ const plant = await plantServer({
179
+ port: config.port || parseInt(env.PORT || '3000', 10),
180
+ basePath,
181
+ translations: config.translations,
182
+ requirePool: config.requirePool,
183
+ stomp: stompFromEnv(env),
184
+ plantFactory: plantFactoryWithOperations(config.plantFactory, ops, sink),
185
+ extraRoutes: siteExtraRoutes(catalog, config.extraRoutes),
186
+ timelineOperator: timelineOperatorFromEnv(catalog, env, config),
187
+ operationDecisions: catalog.decisions,
188
+ owners: config.owners
189
+ });
190
+ return { sink, plant, mqtt, telemetry, operationSync, operatorsSync: catalog.sync };
191
+ }
@@ -0,0 +1,97 @@
1
+ import segmentPipeline, {
2
+ segmentsIngestDestination
3
+ } from '../infrastructure/ingest/pipelines/segmentPipeline.js';
4
+ import decisionPipeline from '../infrastructure/ingest/pipelines/decisionPipeline.js';
5
+ import alertPipeline from '../infrastructure/ingest/pipelines/alertPipeline.js';
6
+ import migrate, { migratePrivileged } from '../infrastructure/ingest/db/migrate.js';
7
+ import stateDataFromPool from '../infrastructure/persistence/stateDataFromPool.js';
8
+ import { parseRequestTimeoutMs } from '../infrastructure/ingest/parseRequestTimeoutMs.js';
9
+ import pg from 'pg';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const appRoot = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ /**
16
+ * Reads deployment environment into a normalized config object.
17
+ *
18
+ * @param {NodeJS.ProcessEnv} env - process environment
19
+ * @returns {object} deployment configuration
20
+ */
21
+ export function readDeploymentConfig(env) {
22
+ const root = path.join(appRoot, '../..');
23
+ return {
24
+ stomp: env.STOMP_URL,
25
+ postgres: env.POSTGRES_URL,
26
+ postgresAdmin: env.POSTGRES_ADMIN_URL,
27
+ apiPort: parseInt(
28
+ env.STATE_HTTP_PORT || env.INTERNAL_API_PORT || '8081',
29
+ 10
30
+ ),
31
+ apiToken: env.STATE_HTTP_TOKEN || env.INTERNAL_API_TOKEN,
32
+ sinkDbProfile: env.SINK_DB_PROFILE === 'central' ? 'central' : 'edge',
33
+ retentionEnabled: env.RETENTION_ENABLED === 'true',
34
+ retentionDays: parseInt(env.RETENTION_DAYS || '30', 10),
35
+ migrationsDir: path.join(root, 'db', 'migrations'),
36
+ requestTimeoutMs: parseRequestTimeoutMs(env.REQUEST_TIMEOUT_MS),
37
+ pipeline: {
38
+ size: parseInt(env.BATCH_SIZE || '50', 10),
39
+ interval: parseInt(env.FLUSH_INTERVAL || '2', 10),
40
+ threshold: parseInt(env.CIRCUIT_THRESHOLD || '5', 10),
41
+ timeout: parseInt(env.CIRCUIT_TIMEOUT || '60', 10),
42
+ poll: parseInt(env.POLL_INTERVAL || '5', 10),
43
+ segmentWindow: parseInt(env.SEGMENT_WINDOW_SECONDS || '15', 10),
44
+ segmentsDestination: env.SEGMENTS_STOMP_DESTINATION ||
45
+ segmentsIngestDestination,
46
+ login: env.STOMP_LOGIN || 'guest',
47
+ passcode: env.STOMP_PASSCODE || 'guest',
48
+ host: env.STOMP_HOST || '/'
49
+ }
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Wires STOMP pipelines, migrations, and state data access from env config.
55
+ * HTTP binding uses edgeApi (metrics, checkpoint, retention).
56
+ *
57
+ * @param {NodeJS.ProcessEnv} [env] - process environment, defaults to process.env
58
+ * @returns {object} sink runtime with pool, dataAccess, and run(httpServer?)
59
+ */
60
+ export default function supervisorSink(env = process.env) {
61
+ const cfg = readDeploymentConfig(env);
62
+ const pool = new pg.Pool({ connectionString: cfg.postgres });
63
+ const segments = segmentPipeline(cfg.stomp, cfg.postgres, pool, cfg.pipeline);
64
+ const decisions = decisionPipeline(cfg.stomp, pool, cfg.pipeline);
65
+ const alerts = alertPipeline(cfg.stomp, pool, cfg.pipeline);
66
+ const metricsEnabled = cfg.sinkDbProfile === 'edge';
67
+ const dataAccess = stateDataFromPool(pool, { metricsEnabled });
68
+ return {
69
+ apiPort: cfg.apiPort,
70
+ apiToken: cfg.apiToken,
71
+ sinkDbProfile: cfg.sinkDbProfile,
72
+ retentionEnabled: cfg.retentionEnabled,
73
+ retentionDays: cfg.retentionDays,
74
+ requestTimeoutMs: cfg.requestTimeoutMs,
75
+ metricsEnabled,
76
+ pool,
77
+ dataAccess,
78
+ async run(httpServer) {
79
+ if (!cfg.postgresAdmin) {
80
+ throw new Error('POSTGRES_ADMIN_URL is required for supervisor-sink database migrations');
81
+ }
82
+ const adminPool = new pg.Pool({ connectionString: cfg.postgresAdmin });
83
+ try {
84
+ await migrate(adminPool, cfg.migrationsDir, cfg.sinkDbProfile);
85
+ await migratePrivileged(adminPool, cfg.migrationsDir, cfg.sinkDbProfile);
86
+ } finally {
87
+ await adminPool.end();
88
+ }
89
+ segments.start();
90
+ decisions.start();
91
+ alerts.start();
92
+ if (httpServer) {
93
+ httpServer.start();
94
+ }
95
+ }
96
+ };
97
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Builds timelineOperator options from env and optional site config.
3
+ *
4
+ * @param {object} catalog - operator routes catalog with provider
5
+ * @param {object} env - process environment
6
+ * @param {object} config - siteServer config with optional anonymousUsers
7
+ * @returns {object} timelineOperator options
8
+ */
9
+ export default function timelineOperatorFromEnv(catalog, env, config) {
10
+ const options = {
11
+ provider: catalog.provider,
12
+ requireOperator: env.REQUIRE_OPERATOR === 'true',
13
+ defaultUser: env.HMI_OPERATOR_USER || 'hmi-kiosk'
14
+ };
15
+ if (config && config.anonymousUsers) {
16
+ options.anonymousUsers = config.anonymousUsers;
17
+ }
18
+ return options;
19
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Supervisor-sink service entry point.
3
+ */
4
+ process.on('unhandledRejection', (err) => {
5
+ process.stderr.write(`Unhandled rejection: ${String(err)}\n`);
6
+ process.exit(1);
7
+ });
8
+
9
+ import supervisorSink from '../application/supervisorSink.js';
10
+ import runRetention from '../infrastructure/ingest/db/runRetention.js';
11
+ import edgeApi from '../infrastructure/http/edge/edgeApi.js';
12
+
13
+ const sink = supervisorSink(process.env);
14
+ const http = edgeApi(sink.dataAccess, {
15
+ port: sink.apiPort,
16
+ token: sink.apiToken,
17
+ metricsEnabled: sink.metricsEnabled,
18
+ retentionEnabled: sink.retentionEnabled,
19
+ retentionDays: sink.retentionDays,
20
+ requestTimeoutMs: sink.requestTimeoutMs,
21
+ pool: sink.pool,
22
+ runRetention
23
+ });
24
+
25
+ sink.run(http).catch((error) => {
26
+ process.stderr.write(`${String(error)}\n`);
27
+ process.exit(1);
28
+ });
@@ -12,7 +12,7 @@
12
12
  * @returns {object} alert with id, message, timestamp, object, event, acknowledge, acknowledged
13
13
  *
14
14
  * @example
15
- * const a = alert('alert-1', 'High voltage', new Date(), 'icht1', srcEvent, ackCallback);
15
+ * const a = alert('alert-1', 'High voltage', new Date(), 'm1', srcEvent, ackCallback);
16
16
  * a.id; // 'alert-1'
17
17
  * a.event; // srcEvent reference
18
18
  * a.acknowledged; // false
@@ -42,7 +42,7 @@ export function alert(id, message, timestamp, object, source, acknowledge) {
42
42
  * @returns {object} acknowledged alert with id, message, timestamp, object, event, acknowledged
43
43
  *
44
44
  * @example
45
- * const a = acknowledgedAlert('alert-1', 'High voltage', new Date(), 'icht1', srcEvent);
45
+ * const a = acknowledgedAlert('alert-1', 'High voltage', new Date(), 'm1', srcEvent);
46
46
  * a.id; // 'alert-1'
47
47
  * a.event; // srcEvent reference
48
48
  * a.acknowledged; // true
@@ -1,4 +1,4 @@
1
- import pubsub from './pubsub.js';
1
+ import pubsub from '../shared/pubsub.js';
2
2
 
3
3
  /**
4
4
  * History of alert occurrences with filtering support.
@@ -11,10 +11,10 @@ import pubsub from './pubsub.js';
11
11
  *
12
12
  * @example
13
13
  * const history = alerts(alert, acknowledgedAlert);
14
- * const a = history.trigger('High voltage', new Date(), 'icht1');
14
+ * const a = history.trigger('High voltage', new Date(), 'm1');
15
15
  * a.id; // 'alert-0'
16
16
  * a.acknowledge(); // replaces with acknowledged version
17
- * history.trigger('From event', new Date(), 'icht1', sourceEvent); // with event
17
+ * history.trigger('From event', new Date(), 'm1', sourceEvent); // with event
18
18
  * history.stream((evt) => console.log(evt)); // subscribe to events
19
19
  */
20
20
  export default function alerts(alert, acknowledgedAlert) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Maps alert ingest status to persistence action.
3
+ *
4
+ * @param {object} record - alert record with status field
5
+ * @returns {string|undefined} action name insert or acknowledge
6
+ *
7
+ * @example
8
+ * alertIngestAction({ status: 'pending' });
9
+ */
10
+ export function alertIngestAction(record) {
11
+ if (record.status === 'pending') {
12
+ return 'insert';
13
+ }
14
+ if (record.status === 'completed') {
15
+ return 'acknowledge';
16
+ }
17
+ return undefined;
18
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Absent ingest poll cursor for first-run backfill.
3
+ *
4
+ * @returns {{ kind: 'empty' }} discriminated union tag without cursor instant
5
+ *
6
+ * @example
7
+ * const cursor = ingestCursorEmpty();
8
+ * cursor.kind;
9
+ */
10
+ export function ingestCursorEmpty() {
11
+ return { kind: 'empty' };
12
+ }
13
+
14
+ /**
15
+ * Positioned ingest poll cursor after a processed source batch.
16
+ *
17
+ * @param {Date} at - last processed source updatedAt instant
18
+ * @returns {{ kind: 'cursor', at: Date }} discriminated union tag with cursor instant
19
+ *
20
+ * @example
21
+ * const cursor = ingestCursorAt(new Date('2024-06-01T10:00:00.000Z'));
22
+ * cursor.at.toISOString();
23
+ */
24
+ export function ingestCursorAt(at) {
25
+ return { kind: 'cursor', at };
26
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Immutable generic machine operation record.
3
+ *
4
+ * @param {string} machine - machine identifier
5
+ * @param {Date} occurredAt - when the operation occurred on the machine
6
+ * @param {string} kind - opaque operation category defined by the plant package
7
+ * @param {string} key - idempotent storage key
8
+ * @param {object} payload - structured operation body
9
+ * @returns {object} operation value with machine, occurredAt, kind, key, payload
10
+ *
11
+ * @example
12
+ * const item = operation('m1', new Date(), 'sample', 'nb-1', { lot: 'A' });
13
+ * item.key;
14
+ */
15
+ export function operation(machine, occurredAt, kind, key, payload) {
16
+ return {
17
+ machine,
18
+ occurredAt,
19
+ kind,
20
+ key,
21
+ payload
22
+ };
23
+ }
@@ -0,0 +1,81 @@
1
+ function stampRow(item, updatedAt) {
2
+ return {
3
+ machine: item.machine,
4
+ occurred_at: item.occurred_at,
5
+ kind: item.kind,
6
+ key: item.key,
7
+ payload: item.payload,
8
+ source_updated_at: updatedAt
9
+ };
10
+ }
11
+
12
+ function loadKind(persistence, sources, machineId, kind, range) {
13
+ const source = sources[kind];
14
+ if (source) {
15
+ return source.list(machineId, range);
16
+ }
17
+ return persistence.listForMachine(machineId, kind, range);
18
+ }
19
+
20
+ function resolveKinds(kinds, sources) {
21
+ if (kinds === undefined) {
22
+ return Object.keys(sources);
23
+ }
24
+ if (Array.isArray(kinds)) {
25
+ return kinds;
26
+ }
27
+ return [kinds];
28
+ }
29
+
30
+ /**
31
+ * Operations wired to persistence, pubsub, and optional non-PG kind sources.
32
+ *
33
+ * listForMachine merges requested kinds from injectable sources and Postgres,
34
+ * sorted by occurred_at. Omitted kinds default to injectable source keys.
35
+ *
36
+ * @param {object} persistence - store with upsert, get, remove, listForMachine
37
+ * @param {object} bus - pubsub instance with stream and emit methods
38
+ * @param {object} [kindSources] - map of kind to { list(machineId, range) }
39
+ * @returns {object} operations with listForMachine, upsert, get, remove, stream
40
+ *
41
+ * @example
42
+ * const ops = operations(store, bus, { temp: temperaturePort });
43
+ * await ops.listForMachine('m1', ['chem', 'temp'], { from: new Date() });
44
+ * await ops.remove('m1', 'nb-1');
45
+ */
46
+ export default function operations(persistence, bus, kindSources) {
47
+ const sources = kindSources || {};
48
+ return {
49
+ listForMachine(machineId, kinds, range) {
50
+ const requested = resolveKinds(kinds, sources);
51
+ if (requested.length === 0) {
52
+ return Promise.resolve([]);
53
+ }
54
+ return Promise.all(requested.map((kind) => {
55
+ return loadKind(persistence, sources, machineId, kind, range);
56
+ })).then((batches) => {
57
+ return batches.flat().sort((left, right) => {
58
+ return new Date(left.occurred_at) - new Date(right.occurred_at);
59
+ });
60
+ });
61
+ },
62
+ upsert(item) {
63
+ const updatedAt = new Date();
64
+ const row = stampRow(item, updatedAt);
65
+ return persistence.upsert(row).then((result) => {
66
+ const type = result.created ? 'created' : 'updated';
67
+ bus.emit({ type, operation: row });
68
+ });
69
+ },
70
+ get(machineId, key) {
71
+ return persistence.get(machineId, key);
72
+ },
73
+ remove(machineId, key) {
74
+ return persistence.remove(machineId, key).then((operation) => {
75
+ bus.emit({ type: 'deleted', operation });
76
+ return operation;
77
+ });
78
+ },
79
+ stream: bus.stream
80
+ };
81
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Immutable operator identity for central audit and HMI card lookup.
3
+ *
4
+ * @param {number} id - database primary key
5
+ * @param {string} cardUid - RFID or badge identifier
6
+ * @param {string} firstName - given name
7
+ * @param {string} lastName - family name
8
+ * @param {string} displayName - full name shown in HMI
9
+ * @returns {object} operator with id, cardUid, firstName, lastName, displayName
10
+ *
11
+ * @example
12
+ * const op = operator(1, 'dev-card-001', 'Ivan', 'Petrov', 'Ivan Petrov');
13
+ * op.displayName;
14
+ */
15
+ export default function operator(id, cardUid, firstName, lastName, displayName) {
16
+ return {
17
+ id,
18
+ cardUid,
19
+ firstName,
20
+ lastName,
21
+ displayName
22
+ };
23
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Generic monitored machine with sensors, timeline, and alerts.
3
+ *
4
+ * @param {string} name - unique machine identifier
5
+ * @param {object} deps - machine dependencies
6
+ * @param {object} deps.sensors - sensor instances keyed by name
7
+ * @param {object} deps.alerts - shop-level alerts collection
8
+ * @param {object} deps.timeline - timeline port with list, retag, pending, respond, stream
9
+ * @returns {object} machine with name, sensors, timeline, alerts methods
10
+ *
11
+ * @example
12
+ * const m = machine('m1', { sensors: { voltage }, alerts: history, timeline: tl });
13
+ * m.name();
14
+ */
15
+ export default function machine(name, deps) {
16
+ const { sensors, alerts: alertHistory, timeline: tl } = deps;
17
+ return {
18
+ name() {
19
+ return name;
20
+ },
21
+ sensors,
22
+ timeline: tl,
23
+ alerts() {
24
+ return alertHistory.all((item) => {
25
+ return item.object === name;
26
+ });
27
+ },
28
+ init() {
29
+ return this;
30
+ }
31
+ };
32
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Top-level plant structure containing shops.
3
+ * Provides initialization for all contained shops.
4
+ *
5
+ * @param {object} shops - initialized list of shops
6
+ * @returns {object} plant with shops property and init method
7
+ *
8
+ * @example
9
+ * const p = plant(initialized({ area: shop }, Object.values));
10
+ * p.init();
11
+ */
12
+ export default function plant(shops, options = {}) {
13
+ return {
14
+ shops,
15
+ operations: options.operations,
16
+ init() {
17
+ shops.init();
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shop containing machines and shop-level alerts.
3
+ *
4
+ * @param {string} name - unique identifier for the shop
5
+ * @param {object} machines - initialized wrapper of machines
6
+ * @param {object} alertHistory - alerts collection for the shop
7
+ * @returns {object} shop with name, machines, alerts properties and init method
8
+ *
9
+ * @example
10
+ * const area = shop('area1', initialized({ m1: machine }, Object.values), alerts());
11
+ * area.init();
12
+ */
13
+ export default function shop(name, machines, alertHistory) {
14
+ return {
15
+ name() {
16
+ return name;
17
+ },
18
+ machines,
19
+ alerts: alertHistory,
20
+ init() {
21
+ machines.init();
22
+ }
23
+ };
24
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Routes normalized segment records to the correct persistence sink.
3
+ *
4
+ * @param {object} segmentSink - batch insert/update sink
5
+ * @param {object} retag - retag sink
6
+ * @param {object} splitSink - split update sink
7
+ * @param {object} closer - orphan open segment closer
8
+ * @returns {object} collector with accept(record)
9
+ *
10
+ * @example
11
+ * const route = segmentDispatch(segmentSink, retag, splitSink, closer);
12
+ * await route.accept({ type: 'segment', machine: 'm1', start_time: '...', duration: 0 });
13
+ */
14
+ export default function segmentDispatch(segmentSink, retag, splitSink, closer) {
15
+ return {
16
+ async accept(record) {
17
+ if (record.type === 'retag') {
18
+ await retag.accept(record);
19
+ } else if (record.type === 'split') {
20
+ await splitSink.write([record]);
21
+ } else if (record.type === 'segment') {
22
+ if (record.duration === 0) {
23
+ await closer.close(record.machine, record.start_time);
24
+ }
25
+ await segmentSink.write([record]);
26
+ } else {
27
+ throw new Error(`Segment command type ${record.type} is not defined`);
28
+ }
29
+ }
30
+ };
31
+ }