@attraccess/plugin-wago 0.1.0-nightly.952.1 → 0.1.0-nightly.954.1
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.
package/dist/index.js
CHANGED
|
@@ -661,9 +661,9 @@ var require_vary = __commonJS({
|
|
|
661
661
|
if (!field2) {
|
|
662
662
|
throw new TypeError("field argument is required");
|
|
663
663
|
}
|
|
664
|
-
var
|
|
665
|
-
for (var j = 0; j <
|
|
666
|
-
if (!FIELD_NAME_REGEXP.test(
|
|
664
|
+
var fields2 = !Array.isArray(field2) ? parse(String(field2)) : field2;
|
|
665
|
+
for (var j = 0; j < fields2.length; j++) {
|
|
666
|
+
if (!FIELD_NAME_REGEXP.test(fields2[j])) {
|
|
667
667
|
throw new TypeError("field argument contains an invalid header name");
|
|
668
668
|
}
|
|
669
669
|
}
|
|
@@ -672,14 +672,14 @@ var require_vary = __commonJS({
|
|
|
672
672
|
}
|
|
673
673
|
var val = header;
|
|
674
674
|
var vals = parse(header.toLowerCase());
|
|
675
|
-
if (
|
|
675
|
+
if (fields2.indexOf("*") !== -1 || vals.indexOf("*") !== -1) {
|
|
676
676
|
return "*";
|
|
677
677
|
}
|
|
678
|
-
for (var i = 0; i <
|
|
679
|
-
var fld =
|
|
678
|
+
for (var i = 0; i < fields2.length; i++) {
|
|
679
|
+
var fld = fields2[i].toLowerCase();
|
|
680
680
|
if (vals.indexOf(fld) === -1) {
|
|
681
681
|
vals.push(fld);
|
|
682
|
-
val = val ? val + ", " +
|
|
682
|
+
val = val ? val + ", " + fields2[i] : fields2[i];
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
return val;
|
|
@@ -2092,11 +2092,11 @@ var require_codes = __commonJS({
|
|
|
2092
2092
|
var require_statuses = __commonJS({
|
|
2093
2093
|
"../../../node_modules/.pnpm/statuses@2.0.2/node_modules/statuses/index.js"(exports2, module2) {
|
|
2094
2094
|
"use strict";
|
|
2095
|
-
var
|
|
2095
|
+
var codes2 = require_codes();
|
|
2096
2096
|
module2.exports = status;
|
|
2097
|
-
status.message =
|
|
2098
|
-
status.code = createMessageToStatusCodeMap(
|
|
2099
|
-
status.codes = createStatusCodeList(
|
|
2097
|
+
status.message = codes2;
|
|
2098
|
+
status.code = createMessageToStatusCodeMap(codes2);
|
|
2099
|
+
status.codes = createStatusCodeList(codes2);
|
|
2100
2100
|
status.redirect = {
|
|
2101
2101
|
300: true,
|
|
2102
2102
|
301: true,
|
|
@@ -2116,17 +2116,17 @@ var require_statuses = __commonJS({
|
|
|
2116
2116
|
503: true,
|
|
2117
2117
|
504: true
|
|
2118
2118
|
};
|
|
2119
|
-
function createMessageToStatusCodeMap(
|
|
2119
|
+
function createMessageToStatusCodeMap(codes3) {
|
|
2120
2120
|
var map = {};
|
|
2121
|
-
Object.keys(
|
|
2122
|
-
var message =
|
|
2121
|
+
Object.keys(codes3).forEach(function forEachCode(code) {
|
|
2122
|
+
var message = codes3[code];
|
|
2123
2123
|
var status2 = Number(code);
|
|
2124
2124
|
map[message.toLowerCase()] = status2;
|
|
2125
2125
|
});
|
|
2126
2126
|
return map;
|
|
2127
2127
|
}
|
|
2128
|
-
function createStatusCodeList(
|
|
2129
|
-
return Object.keys(
|
|
2128
|
+
function createStatusCodeList(codes3) {
|
|
2129
|
+
return Object.keys(codes3).map(function mapCode(code) {
|
|
2130
2130
|
return Number(code);
|
|
2131
2131
|
});
|
|
2132
2132
|
}
|
|
@@ -2358,8 +2358,8 @@ var require_http_errors = __commonJS({
|
|
|
2358
2358
|
Object.defineProperty(func, "name", desc);
|
|
2359
2359
|
}
|
|
2360
2360
|
}
|
|
2361
|
-
function populateConstructorExports(exports3,
|
|
2362
|
-
|
|
2361
|
+
function populateConstructorExports(exports3, codes2, HttpError) {
|
|
2362
|
+
codes2.forEach(function forEachCode(code) {
|
|
2363
2363
|
var CodeError;
|
|
2364
2364
|
var name = toIdentifier(statuses.message[code]);
|
|
2365
2365
|
switch (codeClass(code)) {
|
|
@@ -36301,7 +36301,7 @@ var require_multipart = __commonJS({
|
|
|
36301
36301
|
const fieldsLimit = limits2 && typeof limits2.fields === "number" ? limits2.fields : Infinity;
|
|
36302
36302
|
const partsLimit = limits2 && typeof limits2.parts === "number" ? limits2.parts : Infinity;
|
|
36303
36303
|
let parts = -1;
|
|
36304
|
-
let
|
|
36304
|
+
let fields2 = 0;
|
|
36305
36305
|
let files = 0;
|
|
36306
36306
|
let skipPart = false;
|
|
36307
36307
|
this._fileEndsLeft = 0;
|
|
@@ -36387,7 +36387,7 @@ var require_multipart = __commonJS({
|
|
|
36387
36387
|
}
|
|
36388
36388
|
);
|
|
36389
36389
|
} else {
|
|
36390
|
-
if (
|
|
36390
|
+
if (fields2 === fieldsLimit) {
|
|
36391
36391
|
if (!hitFieldsLimit) {
|
|
36392
36392
|
hitFieldsLimit = true;
|
|
36393
36393
|
this.emit("fieldsLimit");
|
|
@@ -36395,7 +36395,7 @@ var require_multipart = __commonJS({
|
|
|
36395
36395
|
skipPart = true;
|
|
36396
36396
|
return;
|
|
36397
36397
|
}
|
|
36398
|
-
++
|
|
36398
|
+
++fields2;
|
|
36399
36399
|
if (this.listenerCount("field") === 0) {
|
|
36400
36400
|
skipPart = true;
|
|
36401
36401
|
return;
|
|
@@ -38635,7 +38635,7 @@ var require_destroy = __commonJS({
|
|
|
38635
38635
|
var require_errors = __commonJS({
|
|
38636
38636
|
"../../../node_modules/.pnpm/readable-stream@3.6.2/node_modules/readable-stream/errors.js"(exports2, module2) {
|
|
38637
38637
|
"use strict";
|
|
38638
|
-
var
|
|
38638
|
+
var codes2 = {};
|
|
38639
38639
|
function createErrorType(code, message, Base) {
|
|
38640
38640
|
if (!Base) {
|
|
38641
38641
|
Base = Error;
|
|
@@ -38654,7 +38654,7 @@ var require_errors = __commonJS({
|
|
|
38654
38654
|
}
|
|
38655
38655
|
NodeError.prototype.name = Base.name;
|
|
38656
38656
|
NodeError.prototype.code = code;
|
|
38657
|
-
|
|
38657
|
+
codes2[code] = NodeError;
|
|
38658
38658
|
}
|
|
38659
38659
|
function oneOf(expected, thing) {
|
|
38660
38660
|
if (Array.isArray(expected)) {
|
|
@@ -38727,7 +38727,7 @@ var require_errors = __commonJS({
|
|
|
38727
38727
|
return "Unknown encoding: " + arg;
|
|
38728
38728
|
}, TypeError);
|
|
38729
38729
|
createErrorType("ERR_STREAM_UNSHIFT_AFTER_END_EVENT", "stream.unshift() after end event");
|
|
38730
|
-
module2.exports.codes =
|
|
38730
|
+
module2.exports.codes = codes2;
|
|
38731
38731
|
}
|
|
38732
38732
|
});
|
|
38733
38733
|
|
|
@@ -41732,11 +41732,11 @@ var require_multer = __commonJS({
|
|
|
41732
41732
|
this.defParamCharset = options.defParamCharset || "latin1";
|
|
41733
41733
|
this.fileFilter = options.fileFilter || allowAll;
|
|
41734
41734
|
}
|
|
41735
|
-
Multer.prototype._makeMiddleware = function(
|
|
41735
|
+
Multer.prototype._makeMiddleware = function(fields2, fileStrategy) {
|
|
41736
41736
|
function setup() {
|
|
41737
41737
|
var fileFilter = this.fileFilter;
|
|
41738
41738
|
var filesLeft = /* @__PURE__ */ Object.create(null);
|
|
41739
|
-
|
|
41739
|
+
fields2.forEach(function(field2) {
|
|
41740
41740
|
if (typeof field2.maxCount === "number") {
|
|
41741
41741
|
filesLeft[field2.name] = field2.maxCount;
|
|
41742
41742
|
} else {
|
|
@@ -41767,8 +41767,8 @@ var require_multer = __commonJS({
|
|
|
41767
41767
|
Multer.prototype.array = function(name, maxCount) {
|
|
41768
41768
|
return this._makeMiddleware([{ name, maxCount }], "ARRAY");
|
|
41769
41769
|
};
|
|
41770
|
-
Multer.prototype.fields = function(
|
|
41771
|
-
return this._makeMiddleware(
|
|
41770
|
+
Multer.prototype.fields = function(fields2) {
|
|
41771
|
+
return this._makeMiddleware(fields2, "OBJECT");
|
|
41772
41772
|
};
|
|
41773
41773
|
Multer.prototype.none = function() {
|
|
41774
41774
|
return this._makeMiddleware([], "NONE");
|
|
@@ -51633,7 +51633,7 @@ module.exports = __toCommonJS(plugin_exports);
|
|
|
51633
51633
|
|
|
51634
51634
|
// backend/wago.controller.ts
|
|
51635
51635
|
var import_common8 = require("@nestjs/common");
|
|
51636
|
-
var
|
|
51636
|
+
var import_plugins_backend_sdk13 = require("@attraccess/plugins-backend-sdk");
|
|
51637
51637
|
|
|
51638
51638
|
// backend/wago.service.ts
|
|
51639
51639
|
var import_node_crypto3 = require("node:crypto");
|
|
@@ -52047,6 +52047,16 @@ var WAGO_PRESETS = [
|
|
|
52047
52047
|
description: "A monitored digital input foundation."
|
|
52048
52048
|
}
|
|
52049
52049
|
];
|
|
52050
|
+
function applyPreset(snapshot, application) {
|
|
52051
|
+
const preset = application && WAGO_PRESETS.find((item) => item.id === application.presetId);
|
|
52052
|
+
if (!preset) throw new Error("unknown WAGO preset");
|
|
52053
|
+
const channel = presetChannel(application);
|
|
52054
|
+
const existingIndex = snapshot.logicalChannels.findIndex((item) => item.id === application.channelId);
|
|
52055
|
+
const logicalChannels = [...snapshot.logicalChannels];
|
|
52056
|
+
if (existingIndex === -1) logicalChannels.push(channel);
|
|
52057
|
+
else logicalChannels[existingIndex] = channel;
|
|
52058
|
+
return { ...snapshot, logicalChannels };
|
|
52059
|
+
}
|
|
52050
52060
|
function canonicalSnapshot(snapshot) {
|
|
52051
52061
|
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
|
|
52052
52062
|
throw new Error("configuration snapshot must be an object");
|
|
@@ -52066,7 +52076,7 @@ function parseConfigurationReport(value) {
|
|
|
52066
52076
|
if (!Number.isSafeInteger(report.revision) || report.revision < 1 || typeof report.contentHash !== "string" || !report.contentHash.trim() || report.errors !== void 0 && !Array.isArray(report.errors))
|
|
52067
52077
|
return null;
|
|
52068
52078
|
const errors = report.errors ?? [];
|
|
52069
|
-
if (!errors.every(isConfigurationValidationError)) return null;
|
|
52079
|
+
if (!Array.isArray(errors) || !errors.every(isConfigurationValidationError)) return null;
|
|
52070
52080
|
return { revision: report.revision, contentHash: report.contentHash, errors };
|
|
52071
52081
|
}
|
|
52072
52082
|
function validateSnapshot(snapshot) {
|
|
@@ -52097,7 +52107,7 @@ function validateSnapshot(snapshot) {
|
|
|
52097
52107
|
exactKeys(point, path, ["id", "hardwareProfile", "channel"], errors);
|
|
52098
52108
|
addId(point.id, `${path}.id`, pointIds, errors);
|
|
52099
52109
|
enumValue(point.hardwareProfile, `${path}.hardwareProfile`, HARDWARE_PROFILES, errors);
|
|
52100
|
-
if (!Number.isSafeInteger(point.channel) || point.channel < 0)
|
|
52110
|
+
if (typeof point.channel !== "number" || !Number.isSafeInteger(point.channel) || point.channel < 0)
|
|
52101
52111
|
errors.push({
|
|
52102
52112
|
path: `${path}.channel`,
|
|
52103
52113
|
code: "invalid_channel",
|
|
@@ -52337,6 +52347,35 @@ function validateProfile(value, capabilities, path, errors) {
|
|
|
52337
52347
|
})
|
|
52338
52348
|
);
|
|
52339
52349
|
}
|
|
52350
|
+
function presetChannel(application) {
|
|
52351
|
+
const base = {
|
|
52352
|
+
id: application.channelId,
|
|
52353
|
+
physicalPointId: application.physicalPointId,
|
|
52354
|
+
profile: application.presetId,
|
|
52355
|
+
disconnectPolicy: { mode: application.presetId === "generic-monitored-input" ? "hold" : "immediate" }
|
|
52356
|
+
};
|
|
52357
|
+
switch (application.presetId) {
|
|
52358
|
+
case "metered-switched-load":
|
|
52359
|
+
return { ...base, capabilities: ["output", "measurement"], measurement: { unit: "watt", scale: 1, offset: 0 } };
|
|
52360
|
+
case "pulsed-lock-bank":
|
|
52361
|
+
return { ...base, capabilities: ["output", "pulse"], pulse: { durationMs: 500 } };
|
|
52362
|
+
case "guarded-enable-request":
|
|
52363
|
+
if (!application.guardChannelId) throw new Error("guarded enable requests require a guard channel");
|
|
52364
|
+
return {
|
|
52365
|
+
...base,
|
|
52366
|
+
capabilities: ["output", "guard"],
|
|
52367
|
+
guard: { channelId: application.guardChannelId, when: "on" }
|
|
52368
|
+
};
|
|
52369
|
+
case "generic-monitored-input":
|
|
52370
|
+
return { ...base, capabilities: ["input"] };
|
|
52371
|
+
case "generic-digital-output":
|
|
52372
|
+
return {
|
|
52373
|
+
...base,
|
|
52374
|
+
capabilities: application.feedbackChannelId ? ["output", "feedback"] : ["output"],
|
|
52375
|
+
...application.feedbackChannelId ? { feedback: { channelId: application.feedbackChannelId, expected: "match", timeoutMs: 1e3 } } : {}
|
|
52376
|
+
};
|
|
52377
|
+
}
|
|
52378
|
+
}
|
|
52340
52379
|
function referenceError(path, type, id) {
|
|
52341
52380
|
return { path, code: "missing_reference", message: `${type} ${String(id)} does not exist in this snapshot` };
|
|
52342
52381
|
}
|
|
@@ -52348,6 +52387,143 @@ function sortValue(value) {
|
|
|
52348
52387
|
);
|
|
52349
52388
|
}
|
|
52350
52389
|
|
|
52390
|
+
// backend/configuration-digital.ts
|
|
52391
|
+
var DIGITAL_TERMINALS = [
|
|
52392
|
+
...Array.from({ length: 4 }, (_, channel) => ({ channel, label: `DO${channel + 1}`, direction: "output" })),
|
|
52393
|
+
...Array.from({ length: 8 }, (_, index) => ({
|
|
52394
|
+
channel: index + 4,
|
|
52395
|
+
label: `DI${index + 1}`,
|
|
52396
|
+
direction: "input"
|
|
52397
|
+
}))
|
|
52398
|
+
];
|
|
52399
|
+
|
|
52400
|
+
// backend/configuration-editor.ts
|
|
52401
|
+
function editorMetadata(value) {
|
|
52402
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("editor metadata must be an object");
|
|
52403
|
+
const { names, presets } = value;
|
|
52404
|
+
if (!names || typeof names !== "object" || Array.isArray(names) || !Object.values(names).every((name) => typeof name === "string" && name.trim().length > 0 && name.length <= 120))
|
|
52405
|
+
throw new Error("channel names must contain 1\u2013120 characters");
|
|
52406
|
+
if (!Array.isArray(presets) || !presets.every(
|
|
52407
|
+
(preset) => preset && WAGO_PRESETS.some((item) => item.id === preset.presetId) && typeof preset.channelId === "string" && typeof preset.physicalPointId === "string"
|
|
52408
|
+
))
|
|
52409
|
+
throw new Error("preset provenance must contain valid preset applications");
|
|
52410
|
+
return { names, presets };
|
|
52411
|
+
}
|
|
52412
|
+
function validateEditorSnapshot(snapshot) {
|
|
52413
|
+
const errors = validateSnapshot(snapshot);
|
|
52414
|
+
if (errors.length) return errors;
|
|
52415
|
+
const value = snapshot;
|
|
52416
|
+
const terminals = /* @__PURE__ */ new Set();
|
|
52417
|
+
const assignedPoints = /* @__PURE__ */ new Set();
|
|
52418
|
+
value.physicalPoints.forEach((point, index) => {
|
|
52419
|
+
if (point.hardwareProfile !== "751-9301") return;
|
|
52420
|
+
if (!DIGITAL_TERMINALS.some((terminal) => terminal.channel === point.channel))
|
|
52421
|
+
errors.push({
|
|
52422
|
+
path: `physicalPoints[${index}].channel`,
|
|
52423
|
+
code: "unsupported_terminal",
|
|
52424
|
+
message: "select DO1\u2013DO4 or DI1\u2013DI8"
|
|
52425
|
+
});
|
|
52426
|
+
if (terminals.has(point.channel))
|
|
52427
|
+
errors.push({
|
|
52428
|
+
path: `physicalPoints[${index}].channel`,
|
|
52429
|
+
code: "duplicate_terminal",
|
|
52430
|
+
message: "each digital terminal can have only one physical assignment"
|
|
52431
|
+
});
|
|
52432
|
+
terminals.add(point.channel);
|
|
52433
|
+
});
|
|
52434
|
+
value.logicalChannels.forEach((channel, index) => {
|
|
52435
|
+
const path = `logicalChannels[${index}]`;
|
|
52436
|
+
const point = value.physicalPoints.find((point2) => point2.id === channel.physicalPointId);
|
|
52437
|
+
if (point?.hardwareProfile === "751-9301") {
|
|
52438
|
+
const terminal = DIGITAL_TERMINALS.find((terminal2) => terminal2.channel === point.channel);
|
|
52439
|
+
if (terminal && (!channel.capabilities.includes(terminal.direction) || channel.capabilities.includes(terminal.direction === "input" ? "output" : "input")))
|
|
52440
|
+
errors.push({
|
|
52441
|
+
path: `${path}.physicalPointId`,
|
|
52442
|
+
code: "direction_mismatch",
|
|
52443
|
+
message: `${terminal.label} requires an ${terminal.direction} channel`
|
|
52444
|
+
});
|
|
52445
|
+
if (assignedPoints.has(point.id))
|
|
52446
|
+
errors.push({
|
|
52447
|
+
path: `${path}.physicalPointId`,
|
|
52448
|
+
code: "duplicate_assignment",
|
|
52449
|
+
message: "each digital terminal can be assigned to only one logical channel"
|
|
52450
|
+
});
|
|
52451
|
+
assignedPoints.add(point.id);
|
|
52452
|
+
}
|
|
52453
|
+
for (const capability of ["pulse", "guard", "feedback"]) {
|
|
52454
|
+
if (channel.capabilities.includes(capability) && !channel[capability])
|
|
52455
|
+
errors.push({
|
|
52456
|
+
path: `${path}.${capability}`,
|
|
52457
|
+
code: "required_field",
|
|
52458
|
+
message: `configure ${capability} settings for this capability`
|
|
52459
|
+
});
|
|
52460
|
+
}
|
|
52461
|
+
if (channel.guard) {
|
|
52462
|
+
const input = value.logicalChannels.find((item) => item.id === channel.guard?.channelId);
|
|
52463
|
+
if (!input || input.id === channel.id || !input.capabilities.includes("input"))
|
|
52464
|
+
errors.push({
|
|
52465
|
+
path: `${path}.guard.channelId`,
|
|
52466
|
+
code: "invalid_guard_channel",
|
|
52467
|
+
message: "guard must reference a distinct input channel"
|
|
52468
|
+
});
|
|
52469
|
+
}
|
|
52470
|
+
});
|
|
52471
|
+
return errors;
|
|
52472
|
+
}
|
|
52473
|
+
function previewConfigurationPreset(snapshot, application) {
|
|
52474
|
+
const errors = validateSnapshot(snapshot);
|
|
52475
|
+
if (errors.length) throw new Error(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
|
|
52476
|
+
const candidate = applyPreset(snapshot, application);
|
|
52477
|
+
return {
|
|
52478
|
+
draftHash: configurationHash(snapshot),
|
|
52479
|
+
snapshot: candidate,
|
|
52480
|
+
diff: configurationDiff(snapshot, candidate),
|
|
52481
|
+
errors: validateEditorSnapshot(candidate)
|
|
52482
|
+
};
|
|
52483
|
+
}
|
|
52484
|
+
function selectPresetChanges(snapshot, application, paths, expectedHash) {
|
|
52485
|
+
const preview = previewConfigurationPreset(snapshot, application);
|
|
52486
|
+
if (expectedHash !== preview.draftHash) throw new Error("draft changed; preview the preset again");
|
|
52487
|
+
if (!Array.isArray(paths) || paths.some((path) => !preview.diff.some((change) => change.path === path)))
|
|
52488
|
+
throw new Error("unknown preset change");
|
|
52489
|
+
const selected = new Set(paths);
|
|
52490
|
+
function copy(previous, current, path) {
|
|
52491
|
+
if (selected.has(path)) return current;
|
|
52492
|
+
if (Array.isArray(previous) && Array.isArray(current))
|
|
52493
|
+
return Array.from(
|
|
52494
|
+
{ length: Math.max(previous.length, current.length) },
|
|
52495
|
+
(_, index) => copy(previous[index], current[index], `${path}[${index}]`)
|
|
52496
|
+
).filter((item) => item !== void 0);
|
|
52497
|
+
if (previous && current && typeof previous === "object" && typeof current === "object" && !Array.isArray(previous) && !Array.isArray(current)) {
|
|
52498
|
+
const before = previous;
|
|
52499
|
+
const after = current;
|
|
52500
|
+
return Object.fromEntries(
|
|
52501
|
+
[.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].map((key) => [key, copy(before[key], after[key], `${path}.${key}`)]).filter(([, value]) => value !== void 0)
|
|
52502
|
+
);
|
|
52503
|
+
}
|
|
52504
|
+
return previous;
|
|
52505
|
+
}
|
|
52506
|
+
const candidate = copy(snapshot, preview.snapshot, "$");
|
|
52507
|
+
const errors = validateEditorSnapshot(candidate);
|
|
52508
|
+
if (errors.length) throw new Error(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
|
|
52509
|
+
return candidate;
|
|
52510
|
+
}
|
|
52511
|
+
function configurationImpacts(previous, current) {
|
|
52512
|
+
return (previous?.logicalChannels ?? []).flatMap((channel) => {
|
|
52513
|
+
const next = current.logicalChannels.find((item) => item.id === channel.id);
|
|
52514
|
+
const oldPoint = previous?.physicalPoints.find((point) => point.id === channel.physicalPointId);
|
|
52515
|
+
const newPoint = current.physicalPoints.find((point) => point.id === next?.physicalPointId);
|
|
52516
|
+
if (next && configurationHash({ channel, point: oldPoint }) === configurationHash({ channel: next, point: newPoint }))
|
|
52517
|
+
return [];
|
|
52518
|
+
return [
|
|
52519
|
+
{
|
|
52520
|
+
channelId: channel.id,
|
|
52521
|
+
message: next ? "Channel behavior or physical assignment changed. Existing flow commands may behave differently." : "Channel removed. Existing flow references may fail."
|
|
52522
|
+
}
|
|
52523
|
+
];
|
|
52524
|
+
});
|
|
52525
|
+
}
|
|
52526
|
+
|
|
52351
52527
|
// backend/wago-configuration-draft.entity.ts
|
|
52352
52528
|
var import_plugins_backend_sdk4 = require("@attraccess/plugins-backend-sdk");
|
|
52353
52529
|
var WagoConfigurationDraft = class {
|
|
@@ -52382,14 +52558,42 @@ WagoConfigurationDraft = __decorateClass([
|
|
|
52382
52558
|
(0, import_plugins_backend_sdk4.Entity)({ name: "plugin_wago_configuration_drafts" })
|
|
52383
52559
|
], WagoConfigurationDraft);
|
|
52384
52560
|
|
|
52385
|
-
// backend/
|
|
52561
|
+
// backend/configuration-flow-references.ts
|
|
52386
52562
|
var import_plugins_backend_sdk5 = require("@attraccess/plugins-backend-sdk");
|
|
52563
|
+
async function configurationFlowImpacts(context, controllerId, previous, current) {
|
|
52564
|
+
const impacts = configurationImpacts(previous, current);
|
|
52565
|
+
const affected = new Set(impacts.map((impact) => impact.channelId));
|
|
52566
|
+
for (const channel of previous?.logicalChannels ?? []) {
|
|
52567
|
+
if (!affected.has(channel.id) && (channel.guard && affected.has(channel.guard.channelId) || channel.feedback && affected.has(channel.feedback.channelId))) {
|
|
52568
|
+
affected.add(channel.id);
|
|
52569
|
+
impacts.push({ channelId: channel.id, message: "An input used by this channel as a guard or feedback changed." });
|
|
52570
|
+
}
|
|
52571
|
+
}
|
|
52572
|
+
const nodes = await context.dataSource.getRepository(import_plugins_backend_sdk5.ResourceFlowNode).createQueryBuilder("node").select(["node.id", "node.resourceId", "node.type", "node.data"]).where("node.type LIKE :type", { type: "plugin.wago.%" }).andWhere("node.data ->> 'controllerId' = :controllerId", { controllerId }).getMany();
|
|
52573
|
+
for (const node of nodes) {
|
|
52574
|
+
const channelId = node.data?.channelId;
|
|
52575
|
+
if (String(node.type) !== "plugin.wago.command" || typeof channelId !== "string" || !channelId) continue;
|
|
52576
|
+
const message = "Publication changes the configuration revision. Reopen and save this channel's command nodes after it applies.";
|
|
52577
|
+
const impact = impacts.find((item) => item.channelId === channelId);
|
|
52578
|
+
if (impact) {
|
|
52579
|
+
if (!impact.message.includes(message)) impact.message += ` ${message}`;
|
|
52580
|
+
} else impacts.push({ channelId, message });
|
|
52581
|
+
}
|
|
52582
|
+
return impacts.map((impact) => ({
|
|
52583
|
+
...impact,
|
|
52584
|
+
references: nodes.filter((node) => node.data?.channelId === impact.channelId).map((node) => ({ resourceId: node.resourceId, nodeId: node.id, nodeType: node.type }))
|
|
52585
|
+
}));
|
|
52586
|
+
}
|
|
52587
|
+
|
|
52588
|
+
// backend/wago-configuration-revision.entity.ts
|
|
52589
|
+
var import_plugins_backend_sdk6 = require("@attraccess/plugins-backend-sdk");
|
|
52387
52590
|
var WagoConfigurationRevision = class {
|
|
52388
52591
|
constructor() {
|
|
52389
52592
|
__publicField(this, "id");
|
|
52390
52593
|
__publicField(this, "controllerId");
|
|
52391
52594
|
__publicField(this, "revision");
|
|
52392
52595
|
__publicField(this, "snapshot");
|
|
52596
|
+
__publicField(this, "presetProvenance");
|
|
52393
52597
|
__publicField(this, "contentHash");
|
|
52394
52598
|
__publicField(this, "state");
|
|
52395
52599
|
__publicField(this, "rejectionErrors");
|
|
@@ -52398,39 +52602,42 @@ var WagoConfigurationRevision = class {
|
|
|
52398
52602
|
}
|
|
52399
52603
|
};
|
|
52400
52604
|
__decorateClass([
|
|
52401
|
-
(0,
|
|
52605
|
+
(0, import_plugins_backend_sdk6.PrimaryGeneratedColumn)({ type: "integer" })
|
|
52402
52606
|
], WagoConfigurationRevision.prototype, "id", 2);
|
|
52403
52607
|
__decorateClass([
|
|
52404
|
-
(0,
|
|
52608
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "integer", name: "controller_id" })
|
|
52405
52609
|
], WagoConfigurationRevision.prototype, "controllerId", 2);
|
|
52406
52610
|
__decorateClass([
|
|
52407
|
-
(0,
|
|
52611
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "integer" })
|
|
52408
52612
|
], WagoConfigurationRevision.prototype, "revision", 2);
|
|
52409
52613
|
__decorateClass([
|
|
52410
|
-
(0,
|
|
52614
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "text" })
|
|
52411
52615
|
], WagoConfigurationRevision.prototype, "snapshot", 2);
|
|
52412
52616
|
__decorateClass([
|
|
52413
|
-
(0,
|
|
52617
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "text", name: "preset_provenance", nullable: true })
|
|
52618
|
+
], WagoConfigurationRevision.prototype, "presetProvenance", 2);
|
|
52619
|
+
__decorateClass([
|
|
52620
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "content_hash" })
|
|
52414
52621
|
], WagoConfigurationRevision.prototype, "contentHash", 2);
|
|
52415
52622
|
__decorateClass([
|
|
52416
|
-
(0,
|
|
52623
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar" })
|
|
52417
52624
|
], WagoConfigurationRevision.prototype, "state", 2);
|
|
52418
52625
|
__decorateClass([
|
|
52419
|
-
(0,
|
|
52626
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "text", name: "rejection_errors", nullable: true })
|
|
52420
52627
|
], WagoConfigurationRevision.prototype, "rejectionErrors", 2);
|
|
52421
52628
|
__decorateClass([
|
|
52422
|
-
(0,
|
|
52629
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "published_at" })
|
|
52423
52630
|
], WagoConfigurationRevision.prototype, "publishedAt", 2);
|
|
52424
52631
|
__decorateClass([
|
|
52425
|
-
(0,
|
|
52632
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "reported_at", nullable: true })
|
|
52426
52633
|
], WagoConfigurationRevision.prototype, "reportedAt", 2);
|
|
52427
52634
|
WagoConfigurationRevision = __decorateClass([
|
|
52428
|
-
(0,
|
|
52635
|
+
(0, import_plugins_backend_sdk6.Entity)({ name: "plugin_wago_configuration_revisions" })
|
|
52429
52636
|
], WagoConfigurationRevision);
|
|
52430
52637
|
|
|
52431
52638
|
// backend/wago-command-handler.ts
|
|
52432
52639
|
var import_node_crypto2 = require("node:crypto");
|
|
52433
|
-
var
|
|
52640
|
+
var import_plugins_backend_sdk7 = require("@attraccess/plugins-backend-sdk");
|
|
52434
52641
|
var DEFAULT_COMMAND_TIMEOUT_SECONDS = 30;
|
|
52435
52642
|
var MAX_COMMAND_TIMEOUT_SECONDS = 300;
|
|
52436
52643
|
var WagoCommandError = class extends Error {
|
|
@@ -52451,7 +52658,19 @@ var WagoCommandHandler = class {
|
|
|
52451
52658
|
const revision = controllerId ? await this.dependencies.appliedRevision(controllerId) : null;
|
|
52452
52659
|
const snapshot = revision ? JSON.parse(revision.snapshot) : null;
|
|
52453
52660
|
const channelId = typeof config.channelId === "string" ? config.channelId : void 0;
|
|
52454
|
-
const
|
|
52661
|
+
const outputChannels = snapshot?.logicalChannels.filter((item) => item.capabilities.includes("output")) ?? [];
|
|
52662
|
+
let names = {};
|
|
52663
|
+
if (controllerId) {
|
|
52664
|
+
const draft = await this.dependencies.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId });
|
|
52665
|
+
try {
|
|
52666
|
+
const draftMatchesAppliedRevision = typeof draft?.snapshot === "string" && configurationHash(JSON.parse(draft.snapshot)) === revision?.contentHash;
|
|
52667
|
+
const provenance = revision?.presetProvenance ?? (draftMatchesAppliedRevision ? draft?.presetProvenance : null);
|
|
52668
|
+
const storedNames = JSON.parse(provenance ?? "null")?.editor?.names;
|
|
52669
|
+
if (storedNames && typeof storedNames === "object" && !Array.isArray(storedNames)) names = storedNames;
|
|
52670
|
+
} catch {
|
|
52671
|
+
}
|
|
52672
|
+
}
|
|
52673
|
+
const channel = outputChannels.find((item) => item.id === channelId);
|
|
52455
52674
|
const references = channelId && controllerId ? await this.references(controllerId, channelId, resourceId) : [];
|
|
52456
52675
|
const properties = {
|
|
52457
52676
|
controllerId: {
|
|
@@ -52462,16 +52681,20 @@ var WagoCommandHandler = class {
|
|
|
52462
52681
|
const: controller.id,
|
|
52463
52682
|
title: controller.name ?? controller.hardwareId
|
|
52464
52683
|
})),
|
|
52465
|
-
refreshesSchema: true
|
|
52684
|
+
refreshesSchema: true,
|
|
52685
|
+
description: controllerId && !revision ? "Publish a configuration and wait for the controller to apply it before authoring commands." : void 0
|
|
52466
52686
|
}
|
|
52467
52687
|
};
|
|
52468
52688
|
if (controllerId && revision && snapshot) {
|
|
52469
52689
|
properties.channelId = {
|
|
52470
52690
|
type: "string",
|
|
52471
52691
|
title: "Logical Channel",
|
|
52472
|
-
oneOf:
|
|
52692
|
+
oneOf: outputChannels.map((item) => ({
|
|
52693
|
+
const: item.id,
|
|
52694
|
+
title: typeof names[item.id] === "string" ? names[item.id] : `${item.id} (${item.profile})`
|
|
52695
|
+
})),
|
|
52473
52696
|
refreshesSchema: true,
|
|
52474
|
-
|
|
52697
|
+
description: references.length ? `Also controlled by resource flow node${references.length === 1 ? "" : "s"}: ${references.join(", ")}. Reuse is allowed.` : outputChannels.length ? void 0 : "This applied configuration has no output channels. Add an output and publish it first."
|
|
52475
52698
|
};
|
|
52476
52699
|
}
|
|
52477
52700
|
if (channel) {
|
|
@@ -52480,7 +52703,7 @@ var WagoCommandHandler = class {
|
|
|
52480
52703
|
...channel.capabilities.includes("pulse") ? [{ const: "pulse", title: "Pulse" }] : []
|
|
52481
52704
|
] : [];
|
|
52482
52705
|
properties.action = { type: "string", title: "Operation", oneOf: actions, refreshesSchema: true };
|
|
52483
|
-
if (config.action === "set") properties.value = { type: "boolean", title: "State" };
|
|
52706
|
+
if (config.action === "set") properties.value = { type: "boolean", title: "State", default: false };
|
|
52484
52707
|
properties.expectedConfigurationRevision = {
|
|
52485
52708
|
type: "number",
|
|
52486
52709
|
title: "Configuration revision",
|
|
@@ -52521,7 +52744,15 @@ var WagoCommandHandler = class {
|
|
|
52521
52744
|
dynamic: true,
|
|
52522
52745
|
type: "object",
|
|
52523
52746
|
properties,
|
|
52524
|
-
required:
|
|
52747
|
+
required: [
|
|
52748
|
+
.../* @__PURE__ */ new Set([
|
|
52749
|
+
"controllerId",
|
|
52750
|
+
"channelId",
|
|
52751
|
+
"action",
|
|
52752
|
+
"expectedConfigurationRevision",
|
|
52753
|
+
...Object.keys(properties)
|
|
52754
|
+
])
|
|
52755
|
+
]
|
|
52525
52756
|
};
|
|
52526
52757
|
}
|
|
52527
52758
|
async validate(config, validationContext = /* @__PURE__ */ new Map()) {
|
|
@@ -52578,6 +52809,7 @@ var WagoCommandHandler = class {
|
|
|
52578
52809
|
throw new WagoCommandError(`WAGO controller ${controllerId} has no MQTT server`, "transport-dispatch");
|
|
52579
52810
|
const settings = await this.dependencies.getSettings();
|
|
52580
52811
|
const id = (0, import_node_crypto2.randomUUID)();
|
|
52812
|
+
this.dependencies.onCommand?.(controllerId, channelId, id);
|
|
52581
52813
|
const command = JSON.stringify({
|
|
52582
52814
|
id,
|
|
52583
52815
|
expiresAt: new Date(Date.now() + acknowledgementTimeoutSeconds * 1e3).toISOString(),
|
|
@@ -52595,6 +52827,7 @@ var WagoCommandHandler = class {
|
|
|
52595
52827
|
{ qos: 1, retain: false }
|
|
52596
52828
|
);
|
|
52597
52829
|
} catch (error) {
|
|
52830
|
+
this.dependencies.onCommandFailure?.(id, "dispatch-failed");
|
|
52598
52831
|
const dispatchError = new WagoCommandError(
|
|
52599
52832
|
`Failed to publish WAGO command: ${String(error)}`,
|
|
52600
52833
|
"transport-dispatch"
|
|
@@ -52634,7 +52867,7 @@ var WagoCommandHandler = class {
|
|
|
52634
52867
|
}
|
|
52635
52868
|
}
|
|
52636
52869
|
async references(controllerId, channelId, resourceId) {
|
|
52637
|
-
const nodes = await this.dependencies.context.dataSource.getRepository(
|
|
52870
|
+
const nodes = await this.dependencies.context.dataSource.getRepository(import_plugins_backend_sdk7.ResourceFlowNode).createQueryBuilder("node").select(["node.id", "node.resourceId"]).where("node.type = :type", { type: "plugin.wago.command" }).andWhere("node.resourceId <> :resourceId", { resourceId }).andWhere("node.data ->> 'controllerId' = :controllerId", { controllerId }).andWhere("node.data ->> 'channelId' = :channelId", { channelId }).getMany();
|
|
52638
52871
|
return nodes.map((node) => `resource ${node.resourceId} / node ${node.id}`);
|
|
52639
52872
|
}
|
|
52640
52873
|
parse(config) {
|
|
@@ -52716,6 +52949,8 @@ var WagoCommandHandler = class {
|
|
|
52716
52949
|
pending.resolve();
|
|
52717
52950
|
}
|
|
52718
52951
|
reject(id, error) {
|
|
52952
|
+
if (error instanceof WagoCommandError && error.kind === "acknowledgement-timeout")
|
|
52953
|
+
this.dependencies.onCommandFailure?.(id, "timeout");
|
|
52719
52954
|
const pending = this.pending.get(id);
|
|
52720
52955
|
if (!pending) return;
|
|
52721
52956
|
this.pending.delete(id);
|
|
@@ -52734,6 +52969,349 @@ function positiveInteger(value) {
|
|
|
52734
52969
|
return Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
52735
52970
|
}
|
|
52736
52971
|
|
|
52972
|
+
// backend/diagnostics-envelope.ts
|
|
52973
|
+
var DIAGNOSTIC_CATEGORIES = [
|
|
52974
|
+
"heartbeat",
|
|
52975
|
+
"state",
|
|
52976
|
+
"measurements",
|
|
52977
|
+
"faults",
|
|
52978
|
+
"acknowledgements",
|
|
52979
|
+
"configuration/reported"
|
|
52980
|
+
];
|
|
52981
|
+
var MILLI_UNITS = ["milliampere", "millivolt", "milliwatt", "milliwatt-hour", "millipercent"];
|
|
52982
|
+
var CANONICAL_UNITS = [...MILLI_UNITS, "ampere", "volt", "watt", "watt-hour", "percent"];
|
|
52983
|
+
function sourceTime(value) {
|
|
52984
|
+
if (typeof value !== "string") return null;
|
|
52985
|
+
const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,3})?(Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
|
|
52986
|
+
if (!match || Number(match[4] ?? 0) > 23 || Number(match[5] ?? 0) > 59) return null;
|
|
52987
|
+
const time = Date.parse(value);
|
|
52988
|
+
if (!Number.isFinite(time)) return null;
|
|
52989
|
+
const offset = match[2] === "Z" ? 0 : (match[3] === "+" ? 1 : -1) * (Number(match[4]) * 60 + Number(match[5])) * 6e4;
|
|
52990
|
+
return new Date(time + offset).toISOString().slice(0, 19) === match[1] ? time : null;
|
|
52991
|
+
}
|
|
52992
|
+
function emptyStream() {
|
|
52993
|
+
return { lastSourceTime: 0, retiredStreams: [], watermarks: {}, sequenceGaps: 0, trackingExhausted: false };
|
|
52994
|
+
}
|
|
52995
|
+
function canonicalEnvelope(data, category) {
|
|
52996
|
+
return "timestamp" in data || "streamId" in data || category !== "heartbeat" && "sequence" in data;
|
|
52997
|
+
}
|
|
52998
|
+
function validEnvelope(data, now) {
|
|
52999
|
+
const time = sourceTime(data.timestamp);
|
|
53000
|
+
return time !== null && time <= now && typeof data.streamId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.streamId) && Number.isSafeInteger(data.sequence) && data.sequence >= 1;
|
|
53001
|
+
}
|
|
53002
|
+
function admitEnvelope(state, data, category, now) {
|
|
53003
|
+
const stream = data.streamId.toLowerCase();
|
|
53004
|
+
const timestamp = sourceTime(data.timestamp);
|
|
53005
|
+
const sequence = data.sequence;
|
|
53006
|
+
if (state.retiredStreams.includes(stream) || state.trackingExhausted) return "rejected";
|
|
53007
|
+
const restart = state.activeStream !== stream;
|
|
53008
|
+
if (restart) {
|
|
53009
|
+
if (!(category === "heartbeat" || category === "state" && data.connected === true) || now - timestamp > 9e4 || timestamp < state.lastSourceTime)
|
|
53010
|
+
return "rejected";
|
|
53011
|
+
if (state.retiredStreams.length >= 16) {
|
|
53012
|
+
state.trackingExhausted = true;
|
|
53013
|
+
return "rejected";
|
|
53014
|
+
}
|
|
53015
|
+
}
|
|
53016
|
+
const previous = restart ? void 0 : state.watermarks[category];
|
|
53017
|
+
if (previous && (sequence <= previous.sequence || timestamp < previous.timestamp)) return "rejected";
|
|
53018
|
+
if (restart) {
|
|
53019
|
+
if (state.activeStream) state.retiredStreams.push(state.activeStream);
|
|
53020
|
+
state.activeStream = stream;
|
|
53021
|
+
state.watermarks = {};
|
|
53022
|
+
}
|
|
53023
|
+
state.sequenceGaps = Math.min(
|
|
53024
|
+
Number.MAX_SAFE_INTEGER,
|
|
53025
|
+
state.sequenceGaps + Math.max(0, sequence - (previous?.sequence ?? 0) - 1)
|
|
53026
|
+
);
|
|
53027
|
+
state.watermarks[category] = { sequence, timestamp };
|
|
53028
|
+
state.lastSourceTime = Math.max(state.lastSourceTime, timestamp);
|
|
53029
|
+
return restart ? "restart" : "accepted";
|
|
53030
|
+
}
|
|
53031
|
+
|
|
53032
|
+
// backend/diagnostics-validation.ts
|
|
53033
|
+
var fields = new Set(
|
|
53034
|
+
"version physicalPoints logicalChannels id physicalPointId hardwareProfile channel profile capabilities disconnectPolicy mode timeoutMs range minimum maximum pulse durationMs guard channelId when feedback expected measurement unit scale offset".split(
|
|
53035
|
+
" "
|
|
53036
|
+
)
|
|
53037
|
+
);
|
|
53038
|
+
var codes = new Set(
|
|
53039
|
+
"invalid_snapshot unsupported_version invalid_channel invalid_collection invalid_object unknown_field required_field invalid_id duplicate_id unsupported_value invalid_capabilities unsupported_capability duplicate_capability unsupported_field invalid_range invalid_duration invalid_measurement invalid_feedback_channel invalid_timeout missing_capability unknown_reference invalid_reference content_hash_mismatch revision_mismatch hardware_unavailable".split(
|
|
53040
|
+
" "
|
|
53041
|
+
)
|
|
53042
|
+
);
|
|
53043
|
+
function safeValidationSummaries(value) {
|
|
53044
|
+
if (!Array.isArray(value)) return [];
|
|
53045
|
+
return value.slice(0, 50).map((item) => {
|
|
53046
|
+
const error = item && typeof item === "object" ? item : {};
|
|
53047
|
+
const path = typeof error.path === "string" && error.path.length <= 256 ? error.path : "$";
|
|
53048
|
+
const tokens = path.replace(/\[(\d{1,5})\]/g, ".$1").split(".");
|
|
53049
|
+
const safe = tokens[0] === "$" && tokens.slice(1).every((token) => fields.has(token) || /^\d{1,5}$/.test(token));
|
|
53050
|
+
return {
|
|
53051
|
+
path: safe ? path : "$",
|
|
53052
|
+
code: codes.has(error.code) ? error.code : "validation_error"
|
|
53053
|
+
};
|
|
53054
|
+
});
|
|
53055
|
+
}
|
|
53056
|
+
|
|
53057
|
+
// backend/diagnostics-store.ts
|
|
53058
|
+
function freshness(timestamp, now = Date.now(), maxAge = 9e4) {
|
|
53059
|
+
if (!timestamp) return "missing";
|
|
53060
|
+
const time = sourceTime(timestamp);
|
|
53061
|
+
if (time === null) return "invalid";
|
|
53062
|
+
if (time > now) return "future";
|
|
53063
|
+
return now - time > maxAge ? "stale" : "fresh";
|
|
53064
|
+
}
|
|
53065
|
+
var MAX_CONTROLLERS = 256;
|
|
53066
|
+
var MAX_CHANNELS = 256;
|
|
53067
|
+
var RETENTION_MS = 15 * 6e4;
|
|
53068
|
+
var faultCodes = /* @__PURE__ */ new Set([
|
|
53069
|
+
"measurement_read_failed",
|
|
53070
|
+
"device_write_failed",
|
|
53071
|
+
"feedback_mismatch",
|
|
53072
|
+
"feedback_read_failed"
|
|
53073
|
+
]);
|
|
53074
|
+
function identifier(value) {
|
|
53075
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= 256 && !Array.from(value).some((character) => character.charCodeAt(0) < 32);
|
|
53076
|
+
}
|
|
53077
|
+
var WagoDiagnosticsStore = class {
|
|
53078
|
+
constructor(now = Date.now) {
|
|
53079
|
+
__publicField(this, "now", now);
|
|
53080
|
+
__publicField(this, "controllers", /* @__PURE__ */ new Map());
|
|
53081
|
+
__publicField(this, "commands", /* @__PURE__ */ new Map());
|
|
53082
|
+
}
|
|
53083
|
+
prune() {
|
|
53084
|
+
const cutoff = this.now() - RETENTION_MS;
|
|
53085
|
+
for (const [id, state] of this.controllers) if (state.touched < cutoff) this.controllers.delete(id);
|
|
53086
|
+
for (const [id, command] of this.commands) if (command.at < cutoff) this.commands.delete(id);
|
|
53087
|
+
}
|
|
53088
|
+
read(id) {
|
|
53089
|
+
this.prune();
|
|
53090
|
+
const state = this.controllers.get(id);
|
|
53091
|
+
const copy = state ? JSON.parse(JSON.stringify(state)) : {
|
|
53092
|
+
touched: 0,
|
|
53093
|
+
inputs: {},
|
|
53094
|
+
outputs: {},
|
|
53095
|
+
measurements: {},
|
|
53096
|
+
cumulativeMeasurements: {},
|
|
53097
|
+
faults: {},
|
|
53098
|
+
acknowledgements: {},
|
|
53099
|
+
events: [],
|
|
53100
|
+
...emptyStream()
|
|
53101
|
+
};
|
|
53102
|
+
const cutoff = this.now() - RETENTION_MS;
|
|
53103
|
+
for (const field2 of [
|
|
53104
|
+
"inputs",
|
|
53105
|
+
"outputs",
|
|
53106
|
+
"measurements",
|
|
53107
|
+
"cumulativeMeasurements",
|
|
53108
|
+
"faults",
|
|
53109
|
+
"acknowledgements"
|
|
53110
|
+
]) {
|
|
53111
|
+
copy[field2] = Object.assign(/* @__PURE__ */ Object.create(null), copy[field2]);
|
|
53112
|
+
}
|
|
53113
|
+
for (const collection2 of [
|
|
53114
|
+
copy.cumulativeMeasurements,
|
|
53115
|
+
copy.inputs,
|
|
53116
|
+
copy.outputs,
|
|
53117
|
+
copy.measurements,
|
|
53118
|
+
copy.faults,
|
|
53119
|
+
copy.acknowledgements
|
|
53120
|
+
]) {
|
|
53121
|
+
for (const key of Object.keys(collection2))
|
|
53122
|
+
if (Date.parse(collection2[key].receivedAt) < cutoff) delete collection2[key];
|
|
53123
|
+
}
|
|
53124
|
+
copy.events = copy.events.filter((event) => Date.parse(event.receivedAt) >= cutoff);
|
|
53125
|
+
if (copy.rejection && Date.parse(copy.rejection.receivedAt) < cutoff) delete copy.rejection;
|
|
53126
|
+
return copy;
|
|
53127
|
+
}
|
|
53128
|
+
command(controllerId, channelId, id) {
|
|
53129
|
+
this.prune();
|
|
53130
|
+
if (!identifier(channelId) || !identifier(id)) return;
|
|
53131
|
+
if (!this.controllers.has(controllerId) && this.controllers.size >= MAX_CONTROLLERS) return;
|
|
53132
|
+
const state = this.read(controllerId);
|
|
53133
|
+
state.touched = this.now();
|
|
53134
|
+
this.controllers.set(controllerId, state);
|
|
53135
|
+
const oldest = this.commands.keys().next().value;
|
|
53136
|
+
if (this.commands.size >= 1024 && oldest !== void 0) this.commands.delete(oldest);
|
|
53137
|
+
this.commands.set(id, { controllerId, channelId, at: this.now() });
|
|
53138
|
+
}
|
|
53139
|
+
commandFailed(id, status) {
|
|
53140
|
+
this.prune();
|
|
53141
|
+
const command = this.commands.get(id);
|
|
53142
|
+
if (!command) return;
|
|
53143
|
+
this.commands.delete(id);
|
|
53144
|
+
const state = this.controllers.get(command.controllerId);
|
|
53145
|
+
if (!state) return;
|
|
53146
|
+
state.touched = this.now();
|
|
53147
|
+
state.acknowledgements[command.channelId] = { id, status, receivedAt: new Date(this.now()).toISOString() };
|
|
53148
|
+
while (Object.keys(state.acknowledgements).length > MAX_CHANNELS)
|
|
53149
|
+
delete state.acknowledgements[Object.keys(state.acknowledgements)[0]];
|
|
53150
|
+
}
|
|
53151
|
+
canTrack(id) {
|
|
53152
|
+
this.prune();
|
|
53153
|
+
return this.controllers.has(id) || this.controllers.size < MAX_CONTROLLERS || [...this.controllers.values()].some((state) => !state.activeStream);
|
|
53154
|
+
}
|
|
53155
|
+
ingest(id, kind, payload) {
|
|
53156
|
+
this.prune();
|
|
53157
|
+
if (payload.length > 65536) return false;
|
|
53158
|
+
let data;
|
|
53159
|
+
try {
|
|
53160
|
+
data = JSON.parse(payload.toString("utf8"));
|
|
53161
|
+
} catch {
|
|
53162
|
+
return false;
|
|
53163
|
+
}
|
|
53164
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
53165
|
+
if (!DIAGNOSTIC_CATEGORIES.includes(kind)) return false;
|
|
53166
|
+
const canonical = canonicalEnvelope(data, kind);
|
|
53167
|
+
if (canonical && !validEnvelope(data, this.now())) return false;
|
|
53168
|
+
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
53169
|
+
if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
|
|
53170
|
+
return false;
|
|
53171
|
+
if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
|
|
53172
|
+
return false;
|
|
53173
|
+
if (canonical && kind === "configuration/reported" && (!Number.isSafeInteger(data.revision) || data.revision < 1 || typeof data.contentHash !== "string" || !/^[0-9a-f]{64}$/i.test(data.contentHash) || !Array.isArray(data.errors)))
|
|
53174
|
+
return false;
|
|
53175
|
+
if (canonical && kind === "state" && (typeof data.connected !== "boolean" || !(data.revision === null || Number.isSafeInteger(data.revision) && data.revision >= 0) || !isObject(data.outputs) || data.inputs !== void 0 && !isObject(data.inputs) || ![...Object.values(data.outputs), ...Object.values(isObject(data.inputs) ? data.inputs : {})].every(
|
|
53176
|
+
(value) => typeof value === "boolean"
|
|
53177
|
+
)))
|
|
53178
|
+
return false;
|
|
53179
|
+
if (kind === "measurements" && (!identifier(data.channelId) || typeof data.value !== "number" || !Number.isFinite(data.value) || (canonical ? !Number.isSafeInteger(data.value) || !CANONICAL_UNITS.includes(data.unit) || !["live", "cumulative"].includes(data.kind) : !["ampere", "volt", "watt", "percent"].includes(data.unit))))
|
|
53180
|
+
return false;
|
|
53181
|
+
if (kind === "faults" && !identifier(data.channelId)) return false;
|
|
53182
|
+
if (kind === "acknowledgements" && (!identifier(data.id) || !["accepted", "duplicate", "rejected"].includes(data.status)))
|
|
53183
|
+
return false;
|
|
53184
|
+
const oldest = [...this.controllers].find(([, value]) => !value.activeStream)?.[0];
|
|
53185
|
+
if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest === void 0) return false;
|
|
53186
|
+
if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest !== void 0)
|
|
53187
|
+
this.controllers.delete(oldest);
|
|
53188
|
+
const state = this.read(id);
|
|
53189
|
+
if (!canonical && kind === "heartbeat" && typeof data.sequence === "number") {
|
|
53190
|
+
if (data.sequence < (state.legacyHeartbeatSequence ?? 0)) return false;
|
|
53191
|
+
state.legacyHeartbeatSequence = data.sequence;
|
|
53192
|
+
}
|
|
53193
|
+
if (canonical) {
|
|
53194
|
+
if (kind === "measurements" && state.measurementAfter !== void 0 && sourceTime(data.timestamp) <= state.measurementAfter)
|
|
53195
|
+
return false;
|
|
53196
|
+
const previousStream = state.activeStream;
|
|
53197
|
+
const admission = admitEnvelope(state, data, kind, this.now());
|
|
53198
|
+
if (admission === "rejected") {
|
|
53199
|
+
if (state.trackingExhausted) {
|
|
53200
|
+
state.connected = false;
|
|
53201
|
+
this.controllers.set(id, state);
|
|
53202
|
+
}
|
|
53203
|
+
return false;
|
|
53204
|
+
}
|
|
53205
|
+
if (admission === "restart") {
|
|
53206
|
+
state.measurementAfter = sourceTime(data.timestamp) - (previousStream ? 0 : 1);
|
|
53207
|
+
state.inputs = /* @__PURE__ */ Object.create(null);
|
|
53208
|
+
state.outputs = /* @__PURE__ */ Object.create(null);
|
|
53209
|
+
state.measurements = /* @__PURE__ */ Object.create(null);
|
|
53210
|
+
state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
|
|
53211
|
+
state.connected = void 0;
|
|
53212
|
+
state.heartbeatAt = void 0;
|
|
53213
|
+
state.rejection = void 0;
|
|
53214
|
+
state.hardwareAvailable = void 0;
|
|
53215
|
+
state.stateSourceAt = void 0;
|
|
53216
|
+
state.revision = void 0;
|
|
53217
|
+
state.contentHash = void 0;
|
|
53218
|
+
}
|
|
53219
|
+
} else if (state.activeStream && !["heartbeat", "configuration/reported"].includes(kind)) {
|
|
53220
|
+
if (kind === "state" && data.connected === false) {
|
|
53221
|
+
state.connected = false;
|
|
53222
|
+
state.measurementAfter = this.now();
|
|
53223
|
+
state.stateSourceAt = void 0;
|
|
53224
|
+
this.controllers.set(id, state);
|
|
53225
|
+
}
|
|
53226
|
+
return false;
|
|
53227
|
+
}
|
|
53228
|
+
state.touched = this.now();
|
|
53229
|
+
const receivedAt = new Date(this.now()).toISOString();
|
|
53230
|
+
const metadata = {
|
|
53231
|
+
sourceAt: canonical ? data.timestamp : null,
|
|
53232
|
+
receivedAt,
|
|
53233
|
+
streamId: canonical ? state.activeStream : null,
|
|
53234
|
+
sequence: canonical ? data.sequence : null
|
|
53235
|
+
};
|
|
53236
|
+
if (kind === "heartbeat") state.heartbeatAt = canonical ? data.timestamp : receivedAt;
|
|
53237
|
+
if (kind === "state") {
|
|
53238
|
+
const contentHash = typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash) ? data.contentHash : void 0;
|
|
53239
|
+
const hardwareAvailable = isObject(data.readiness) && typeof data.readiness.hardwareAvailable === "boolean" ? data.readiness.hardwareAvailable : void 0;
|
|
53240
|
+
if (state.connected !== data.connected || state.revision !== data.revision || state.contentHash !== contentHash || state.hardwareAvailable !== hardwareAvailable) {
|
|
53241
|
+
if (canonical || state.stateSourceAt)
|
|
53242
|
+
state.measurementAfter = canonical ? sourceTime(data.timestamp) : this.now();
|
|
53243
|
+
state.measurements = /* @__PURE__ */ Object.create(null);
|
|
53244
|
+
state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
|
|
53245
|
+
}
|
|
53246
|
+
state.inputs = /* @__PURE__ */ Object.create(null);
|
|
53247
|
+
state.outputs = /* @__PURE__ */ Object.create(null);
|
|
53248
|
+
if (typeof data.connected === "boolean") state.connected = data.connected;
|
|
53249
|
+
state.revision = Number.isSafeInteger(data.revision) ? data.revision : void 0;
|
|
53250
|
+
state.contentHash = contentHash;
|
|
53251
|
+
state.hardwareAvailable = hardwareAvailable;
|
|
53252
|
+
state.stateSourceAt = canonical ? data.timestamp : void 0;
|
|
53253
|
+
if (data.outputs && typeof data.outputs === "object" && !Array.isArray(data.outputs)) {
|
|
53254
|
+
for (const [channelId, value] of Object.entries(data.outputs).slice(0, MAX_CHANNELS)) {
|
|
53255
|
+
if (identifier(channelId) && typeof value === "boolean")
|
|
53256
|
+
state.outputs[channelId] = { kind: "output", value, ...metadata };
|
|
53257
|
+
}
|
|
53258
|
+
}
|
|
53259
|
+
if (isObject(data.inputs))
|
|
53260
|
+
for (const [channelId, value] of Object.entries(data.inputs).slice(0, MAX_CHANNELS)) {
|
|
53261
|
+
if (identifier(channelId) && typeof value === "boolean")
|
|
53262
|
+
state.inputs[channelId] = { kind: "input", value, ...metadata };
|
|
53263
|
+
}
|
|
53264
|
+
}
|
|
53265
|
+
if (kind === "measurements" && identifier(data.channelId) && typeof data.value === "number" && Number.isFinite(data.value) && (canonical ? CANONICAL_UNITS : ["ampere", "volt", "watt", "percent"]).includes(data.unit)) {
|
|
53266
|
+
const collection2 = canonical && data.kind === "cumulative" ? state.cumulativeMeasurements : state.measurements;
|
|
53267
|
+
collection2[data.channelId] = {
|
|
53268
|
+
kind: "measurement",
|
|
53269
|
+
value: data.value,
|
|
53270
|
+
unit: data.unit,
|
|
53271
|
+
...canonical ? { measurementKind: data.kind } : {},
|
|
53272
|
+
...metadata
|
|
53273
|
+
};
|
|
53274
|
+
}
|
|
53275
|
+
if (kind === "faults" && identifier(data.channelId))
|
|
53276
|
+
state.faults[data.channelId] = {
|
|
53277
|
+
code: faultCodes.has(data.code) ? data.code : "runtime_fault",
|
|
53278
|
+
receivedAt
|
|
53279
|
+
};
|
|
53280
|
+
if (kind === "acknowledgements" && identifier(data.id) && ["accepted", "duplicate", "rejected"].includes(data.status)) {
|
|
53281
|
+
const command = this.commands.get(data.id);
|
|
53282
|
+
if (command?.controllerId === id) {
|
|
53283
|
+
state.acknowledgements[command.channelId] = {
|
|
53284
|
+
id: data.id,
|
|
53285
|
+
status: data.status,
|
|
53286
|
+
receivedAt
|
|
53287
|
+
};
|
|
53288
|
+
this.commands.delete(data.id);
|
|
53289
|
+
}
|
|
53290
|
+
}
|
|
53291
|
+
if (kind === "configuration/reported" && Number.isSafeInteger(data.revision) && typeof data.contentHash === "string")
|
|
53292
|
+
state.rejection = {
|
|
53293
|
+
revision: data.revision,
|
|
53294
|
+
contentHash: data.contentHash,
|
|
53295
|
+
receivedAt,
|
|
53296
|
+
errors: safeValidationSummaries(data.errors)
|
|
53297
|
+
};
|
|
53298
|
+
for (const collection2 of [
|
|
53299
|
+
state.cumulativeMeasurements,
|
|
53300
|
+
state.inputs,
|
|
53301
|
+
state.outputs,
|
|
53302
|
+
state.measurements,
|
|
53303
|
+
state.faults,
|
|
53304
|
+
state.acknowledgements
|
|
53305
|
+
]) {
|
|
53306
|
+
while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
|
|
53307
|
+
}
|
|
53308
|
+
state.events.push({ kind, receivedAt });
|
|
53309
|
+
state.events = state.events.slice(-50);
|
|
53310
|
+
this.controllers.set(id, state);
|
|
53311
|
+
return true;
|
|
53312
|
+
}
|
|
53313
|
+
};
|
|
53314
|
+
|
|
52737
53315
|
// backend/wago.service.ts
|
|
52738
53316
|
var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
|
|
52739
53317
|
var STALE_AFTER_MS = 9e4;
|
|
@@ -52748,6 +53326,7 @@ var MqttSubscriptionError = class extends Error {
|
|
|
52748
53326
|
var WagoService = class {
|
|
52749
53327
|
constructor(context) {
|
|
52750
53328
|
__publicField(this, "context", context);
|
|
53329
|
+
__publicField(this, "diagnostics", new WagoDiagnosticsStore());
|
|
52751
53330
|
__publicField(this, "controllers");
|
|
52752
53331
|
__publicField(this, "settings");
|
|
52753
53332
|
__publicField(this, "enrollments");
|
|
@@ -52765,7 +53344,9 @@ var WagoService = class {
|
|
|
52765
53344
|
controllers: () => this.controllers,
|
|
52766
53345
|
claimedController: (id) => this.claimedController(id),
|
|
52767
53346
|
getSettings: () => this.getSettings(),
|
|
52768
|
-
appliedRevision: (id) => this.appliedRevision(id)
|
|
53347
|
+
appliedRevision: (id) => this.appliedRevision(id),
|
|
53348
|
+
onCommand: (controllerId, channelId, id) => this.diagnostics.command(controllerId, channelId, id),
|
|
53349
|
+
onCommandFailure: (id, status) => this.diagnostics.commandFailed(id, status)
|
|
52769
53350
|
}));
|
|
52770
53351
|
__publicField(this, "claimConfigurationLock", Promise.resolve());
|
|
52771
53352
|
__publicField(this, "subscriptionRebuild", Promise.resolve());
|
|
@@ -52829,7 +53410,7 @@ var WagoService = class {
|
|
|
52829
53410
|
runtimeVersion: controller.runtimeVersion,
|
|
52830
53411
|
capabilities: controller.capabilities,
|
|
52831
53412
|
lastSequence: controller.lastSequence,
|
|
52832
|
-
lastHeartbeatAt: controller.lastHeartbeatAt,
|
|
53413
|
+
lastHeartbeatAt: this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt,
|
|
52833
53414
|
lastSeenAt: controller.lastSeenAt,
|
|
52834
53415
|
compatibilityError: controller.compatibilityError,
|
|
52835
53416
|
createdAt: controller.createdAt,
|
|
@@ -52875,13 +53456,40 @@ var WagoService = class {
|
|
|
52875
53456
|
await this.claimedController(controllerId);
|
|
52876
53457
|
return this.drafts.findOneBy({ controllerId });
|
|
52877
53458
|
}
|
|
52878
|
-
async saveDraft(controllerId, snapshot) {
|
|
52879
|
-
return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot));
|
|
53459
|
+
async saveDraft(controllerId, snapshot, metadata) {
|
|
53460
|
+
return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot, metadata));
|
|
52880
53461
|
}
|
|
52881
|
-
|
|
53462
|
+
presets() {
|
|
53463
|
+
return WAGO_PRESETS;
|
|
53464
|
+
}
|
|
53465
|
+
async previewPreset(controllerId, application, snapshot) {
|
|
52882
53466
|
const draft = await this.getDraft(controllerId);
|
|
52883
|
-
|
|
52884
|
-
|
|
53467
|
+
const source = snapshot ?? (draft ? JSON.parse(draft.snapshot) : { version: 1, physicalPoints: [], logicalChannels: [] });
|
|
53468
|
+
try {
|
|
53469
|
+
return previewConfigurationPreset(source, application);
|
|
53470
|
+
} catch (error) {
|
|
53471
|
+
throw new import_common.BadRequestException(error instanceof Error ? error.message : "invalid preset");
|
|
53472
|
+
}
|
|
53473
|
+
}
|
|
53474
|
+
async applyPreset(controllerId, application, selectedPaths, previewedDraftHash, snapshot) {
|
|
53475
|
+
return this.withConfigurationLock(controllerId, async () => {
|
|
53476
|
+
const draft = await this.getDraft(controllerId);
|
|
53477
|
+
const source = snapshot ?? (draft ? JSON.parse(draft.snapshot) : { version: 1, physicalPoints: [], logicalChannels: [] });
|
|
53478
|
+
let candidate;
|
|
53479
|
+
try {
|
|
53480
|
+
candidate = selectPresetChanges(source, application, selectedPaths, previewedDraftHash);
|
|
53481
|
+
} catch (error) {
|
|
53482
|
+
throw new import_common.BadRequestException(error instanceof Error ? error.message : "invalid preset");
|
|
53483
|
+
}
|
|
53484
|
+
if (snapshot) return { snapshot: canonicalSnapshot(candidate) };
|
|
53485
|
+
return this.saveDraftWhileLocked(controllerId, candidate);
|
|
53486
|
+
});
|
|
53487
|
+
}
|
|
53488
|
+
async validateDraft(controllerId, snapshot) {
|
|
53489
|
+
const draft = await this.getDraft(controllerId);
|
|
53490
|
+
if (!draft && snapshot === void 0)
|
|
53491
|
+
throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
53492
|
+
const errors = validateEditorSnapshot(snapshot === void 0 && draft ? JSON.parse(draft.snapshot) : snapshot);
|
|
52885
53493
|
return { valid: errors.length === 0, errors };
|
|
52886
53494
|
}
|
|
52887
53495
|
async revisionsFor(controllerId, offset = 0, limit = 20) {
|
|
@@ -52899,7 +53507,8 @@ var WagoService = class {
|
|
|
52899
53507
|
"state",
|
|
52900
53508
|
"rejectionErrors",
|
|
52901
53509
|
"publishedAt",
|
|
52902
|
-
"reportedAt"
|
|
53510
|
+
"reportedAt",
|
|
53511
|
+
"presetProvenance"
|
|
52903
53512
|
],
|
|
52904
53513
|
skip: pageOffset,
|
|
52905
53514
|
take: pageLimit
|
|
@@ -52909,35 +53518,93 @@ var WagoService = class {
|
|
|
52909
53518
|
async reviewDraft(controllerId) {
|
|
52910
53519
|
return this.withConfigurationLock(controllerId, () => this.reviewDraftWhileLocked(controllerId));
|
|
52911
53520
|
}
|
|
52912
|
-
async publishDraft(controllerId) {
|
|
52913
|
-
return this.withConfigurationLock(
|
|
53521
|
+
async publishDraft(controllerId, force = false, reviewedHash) {
|
|
53522
|
+
return this.withConfigurationLock(
|
|
53523
|
+
controllerId,
|
|
53524
|
+
() => this.publishDraftWhileLocked(controllerId, force, reviewedHash)
|
|
53525
|
+
);
|
|
52914
53526
|
}
|
|
52915
|
-
async rollback(controllerId, revision) {
|
|
53527
|
+
async rollback(controllerId, revision, force = false, sourceHash, currentHash, draftHash, impactHash) {
|
|
52916
53528
|
return this.withConfigurationLock(controllerId, async () => {
|
|
53529
|
+
await this.claimedController(controllerId);
|
|
52917
53530
|
const source = await this.revisions.findOneBy({ controllerId, revision });
|
|
52918
53531
|
if (!source) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
|
|
52919
|
-
await this.
|
|
53532
|
+
const current = await this.latestRevision(controllerId);
|
|
53533
|
+
const draft = await this.drafts.findOneBy({ controllerId });
|
|
53534
|
+
if (draftHash !== this.draftIdentity(draft) || sourceHash !== source.contentHash || currentHash !== this.revisionIdentity(current))
|
|
53535
|
+
throw new import_common.ConflictException("configuration changed since rollback preview; preview and confirm again");
|
|
53536
|
+
const snapshot = JSON.parse(source.snapshot);
|
|
53537
|
+
const errors = validateEditorSnapshot(snapshot);
|
|
53538
|
+
if (errors.length) throw new import_common.ConflictException({ message: "rollback configuration is invalid", errors });
|
|
53539
|
+
await this.requireImpactAcknowledgement(controllerId, current, snapshot, force, impactHash);
|
|
53540
|
+
await this.saveDraftWhileLocked(
|
|
53541
|
+
controllerId,
|
|
53542
|
+
snapshot,
|
|
53543
|
+
(source.presetProvenance ? JSON.parse(source.presetProvenance).editor : null) ?? { names: {}, presets: [] }
|
|
53544
|
+
);
|
|
52920
53545
|
await this.reviewDraftWhileLocked(controllerId);
|
|
52921
|
-
return this.publishDraftWhileLocked(controllerId);
|
|
53546
|
+
return this.publishDraftWhileLocked(controllerId, force);
|
|
52922
53547
|
});
|
|
52923
53548
|
}
|
|
52924
53549
|
async previewRevision(controllerId, revision) {
|
|
52925
|
-
|
|
52926
|
-
|
|
52927
|
-
|
|
52928
|
-
|
|
52929
|
-
|
|
52930
|
-
|
|
52931
|
-
|
|
52932
|
-
|
|
52933
|
-
|
|
53550
|
+
return this.withConfigurationLock(controllerId, async () => {
|
|
53551
|
+
await this.claimedController(controllerId);
|
|
53552
|
+
const selected = await this.revisions.findOneBy({ controllerId, revision });
|
|
53553
|
+
if (!selected) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
|
|
53554
|
+
const [current] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
|
|
53555
|
+
const draft = await this.drafts.findOneBy({ controllerId });
|
|
53556
|
+
const impacts = await configurationFlowImpacts(
|
|
53557
|
+
this.context,
|
|
53558
|
+
controllerId,
|
|
53559
|
+
current ? JSON.parse(current.snapshot) : null,
|
|
53560
|
+
JSON.parse(selected.snapshot)
|
|
53561
|
+
);
|
|
53562
|
+
return {
|
|
53563
|
+
draftHash: this.draftIdentity(draft),
|
|
53564
|
+
impacts,
|
|
53565
|
+
revision: selected,
|
|
53566
|
+
current: current ?? null,
|
|
53567
|
+
diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, JSON.parse(selected.snapshot)),
|
|
53568
|
+
metadataDiff: configurationDiff(
|
|
53569
|
+
this.metadataFromProvenance(current?.presetProvenance),
|
|
53570
|
+
this.metadataFromProvenance(selected.presetProvenance)
|
|
53571
|
+
),
|
|
53572
|
+
impactHash: configurationHash({ impacts })
|
|
53573
|
+
};
|
|
53574
|
+
});
|
|
53575
|
+
}
|
|
53576
|
+
draftIdentity(draft) {
|
|
53577
|
+
return configurationHash(draft ? { snapshot: draft.snapshot, metadata: draft.presetProvenance ?? null } : null);
|
|
53578
|
+
}
|
|
53579
|
+
revisionIdentity(revision) {
|
|
53580
|
+
return revision ? `${revision.revision}:${revision.contentHash}` : null;
|
|
52934
53581
|
}
|
|
52935
|
-
|
|
53582
|
+
reviewIdentity(draft, impacts) {
|
|
53583
|
+
return configurationHash({ draft: this.draftIdentity(draft), impacts });
|
|
53584
|
+
}
|
|
53585
|
+
metadataFromProvenance(provenance) {
|
|
53586
|
+
if (!provenance) return { names: {}, presets: [] };
|
|
53587
|
+
try {
|
|
53588
|
+
return editorMetadata(JSON.parse(provenance).editor);
|
|
53589
|
+
} catch {
|
|
53590
|
+
return { names: {}, presets: [] };
|
|
53591
|
+
}
|
|
53592
|
+
}
|
|
53593
|
+
async saveDraftWhileLocked(controllerId, snapshot, metadata) {
|
|
52936
53594
|
await this.claimedController(controllerId);
|
|
53595
|
+
let provenance;
|
|
53596
|
+
if (metadata !== void 0) {
|
|
53597
|
+
try {
|
|
53598
|
+
provenance = JSON.stringify({ editor: editorMetadata(metadata) });
|
|
53599
|
+
} catch (error) {
|
|
53600
|
+
throw new import_common.BadRequestException(error instanceof Error ? error.message : "invalid editor metadata");
|
|
53601
|
+
}
|
|
53602
|
+
}
|
|
52937
53603
|
const serialized = canonicalSnapshot(snapshot);
|
|
52938
53604
|
const existing = await this.drafts.findOneBy({ controllerId });
|
|
52939
53605
|
const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, updatedAt: "" });
|
|
52940
53606
|
draft.snapshot = serialized;
|
|
53607
|
+
if (provenance !== void 0) draft.presetProvenance = provenance;
|
|
52941
53608
|
draft.reviewedHash = null;
|
|
52942
53609
|
draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
52943
53610
|
return this.drafts.save(draft);
|
|
@@ -52947,28 +53614,66 @@ var WagoService = class {
|
|
|
52947
53614
|
const draft = await this.drafts.findOneBy({ controllerId });
|
|
52948
53615
|
if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
52949
53616
|
const previous = await this.latestRevision(controllerId);
|
|
52950
|
-
|
|
53617
|
+
const impacts = await configurationFlowImpacts(
|
|
53618
|
+
this.context,
|
|
53619
|
+
controllerId,
|
|
53620
|
+
previous ? JSON.parse(previous.snapshot) : null,
|
|
53621
|
+
JSON.parse(draft.snapshot)
|
|
53622
|
+
);
|
|
53623
|
+
draft.reviewedHash = this.reviewIdentity(draft, impacts);
|
|
52951
53624
|
await this.drafts.save(draft);
|
|
52952
53625
|
const diff = configurationDiff(previous ? JSON.parse(previous.snapshot) : null, JSON.parse(draft.snapshot));
|
|
52953
|
-
|
|
53626
|
+
const metadataDiff = configurationDiff(
|
|
53627
|
+
this.metadataFromProvenance(previous?.presetProvenance),
|
|
53628
|
+
this.metadataFromProvenance(draft.presetProvenance)
|
|
53629
|
+
);
|
|
53630
|
+
return {
|
|
53631
|
+
draft,
|
|
53632
|
+
previous,
|
|
53633
|
+
changed: diff.length > 0 || metadataDiff.length > 0,
|
|
53634
|
+
diff,
|
|
53635
|
+
metadataDiff,
|
|
53636
|
+
impacts
|
|
53637
|
+
};
|
|
52954
53638
|
}
|
|
52955
|
-
async
|
|
53639
|
+
async requireImpactAcknowledgement(controllerId, previous, snapshot, force, expectedImpactHash) {
|
|
53640
|
+
const impacts = await configurationFlowImpacts(
|
|
53641
|
+
this.context,
|
|
53642
|
+
controllerId,
|
|
53643
|
+
previous ? JSON.parse(previous.snapshot) : null,
|
|
53644
|
+
snapshot
|
|
53645
|
+
);
|
|
53646
|
+
if (impacts.length && !force)
|
|
53647
|
+
throw new import_common.ConflictException({ message: "acknowledge potential flow impacts before publishing", impacts });
|
|
53648
|
+
if (force && expectedImpactHash !== void 0 && expectedImpactHash !== configurationHash({ impacts }))
|
|
53649
|
+
throw new import_common.ConflictException({
|
|
53650
|
+
message: "flow impacts changed since preview; preview and confirm again",
|
|
53651
|
+
impacts
|
|
53652
|
+
});
|
|
53653
|
+
return impacts;
|
|
53654
|
+
}
|
|
53655
|
+
async publishDraftWhileLocked(controllerId, force = false, reviewedHash) {
|
|
52956
53656
|
const controller = await this.claimedController(controllerId);
|
|
52957
53657
|
const draft = await this.drafts.findOneBy({ controllerId });
|
|
52958
53658
|
if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
52959
|
-
const validation =
|
|
53659
|
+
const validation = validateEditorSnapshot(JSON.parse(draft.snapshot));
|
|
52960
53660
|
if (validation.length)
|
|
52961
53661
|
throw new import_common.ConflictException({ message: "configuration draft is invalid", errors: validation });
|
|
52962
53662
|
const contentHash = configurationHash(JSON.parse(draft.snapshot));
|
|
52963
|
-
if (draft.reviewedHash !== contentHash)
|
|
52964
|
-
throw new import_common.ConflictException("review the current configuration draft before publishing it");
|
|
52965
53663
|
const previous = await this.latestRevision(controllerId);
|
|
52966
|
-
|
|
53664
|
+
const impacts = await this.requireImpactAcknowledgement(controllerId, previous, JSON.parse(draft.snapshot), force);
|
|
53665
|
+
const reviewIdentity = this.reviewIdentity(draft, impacts);
|
|
53666
|
+
if (reviewedHash !== void 0 && reviewedHash !== reviewIdentity)
|
|
53667
|
+
throw new import_common.ConflictException("draft changed since your review; review it again");
|
|
53668
|
+
if (draft.reviewedHash !== reviewIdentity)
|
|
53669
|
+
throw new import_common.ConflictException("review the current configuration draft before publishing it");
|
|
53670
|
+
if (previous?.state === "pending" && previous.contentHash === contentHash && (previous.presetProvenance ?? null) === (draft.presetProvenance ?? null))
|
|
52967
53671
|
return this.publishRevision(controller, previous);
|
|
52968
53672
|
const revision = this.revisions.create({
|
|
52969
53673
|
controllerId,
|
|
52970
53674
|
revision: (previous?.revision ?? 0) + 1,
|
|
52971
53675
|
snapshot: draft.snapshot,
|
|
53676
|
+
presetProvenance: draft.presetProvenance ?? null,
|
|
52972
53677
|
contentHash,
|
|
52973
53678
|
state: "pending",
|
|
52974
53679
|
rejectionErrors: null,
|
|
@@ -53284,6 +53989,7 @@ var WagoService = class {
|
|
|
53284
53989
|
});
|
|
53285
53990
|
const generation = this.activeSubscriptionGeneration + 1;
|
|
53286
53991
|
const replacements = [];
|
|
53992
|
+
const retainedStates = /* @__PURE__ */ new Map();
|
|
53287
53993
|
try {
|
|
53288
53994
|
for (const serverId of serverIds) {
|
|
53289
53995
|
replacements.push(
|
|
@@ -53310,7 +54016,10 @@ var WagoService = class {
|
|
|
53310
54016
|
if (!this.isActiveSubscriptionGeneration(generation)) return;
|
|
53311
54017
|
const hardwareId = configurationReportedHardwareId(settings.operationalPrefix, message.topic);
|
|
53312
54018
|
const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
|
|
53313
|
-
if (controller)
|
|
54019
|
+
if (controller) {
|
|
54020
|
+
this.diagnostics.ingest(controller.id, "configuration/reported", message.payload);
|
|
54021
|
+
this.enqueueConfigurationReport(controller.id, message.payload);
|
|
54022
|
+
}
|
|
53314
54023
|
}
|
|
53315
54024
|
)
|
|
53316
54025
|
);
|
|
@@ -53319,7 +54028,10 @@ var WagoService = class {
|
|
|
53319
54028
|
if (!this.isActiveSubscriptionGeneration(generation)) return;
|
|
53320
54029
|
const hardwareId = acknowledgementHardwareId(settings.operationalPrefix, message.topic);
|
|
53321
54030
|
const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
|
|
53322
|
-
if (controller)
|
|
54031
|
+
if (controller) {
|
|
54032
|
+
this.diagnostics.ingest(controller.id, "acknowledgements", message.payload);
|
|
54033
|
+
this.onCommandAcknowledgement(controller.id, message.payload);
|
|
54034
|
+
}
|
|
53323
54035
|
})
|
|
53324
54036
|
);
|
|
53325
54037
|
if (this.destroyed) {
|
|
@@ -53327,6 +54039,20 @@ var WagoService = class {
|
|
|
53327
54039
|
return;
|
|
53328
54040
|
}
|
|
53329
54041
|
for (const controller of claimedControllers) {
|
|
54042
|
+
for (const suffix of ["state", "measurements", "faults"]) {
|
|
54043
|
+
replacements.push(
|
|
54044
|
+
await this.subscribeMqtt(
|
|
54045
|
+
serverId,
|
|
54046
|
+
`${settings.operationalPrefix}/v1/controllers/${controller.hardwareId}/${suffix}`,
|
|
54047
|
+
(message) => {
|
|
54048
|
+
if (this.isActiveSubscriptionGeneration(generation))
|
|
54049
|
+
this.diagnostics.ingest(controller.id, suffix, message.payload);
|
|
54050
|
+
else if (!this.destroyed && suffix === "state" && message.payload.length <= 65536)
|
|
54051
|
+
retainedStates.set(controller.id, Buffer.from(message.payload));
|
|
54052
|
+
}
|
|
54053
|
+
)
|
|
54054
|
+
);
|
|
54055
|
+
}
|
|
53330
54056
|
replacements.push(
|
|
53331
54057
|
await this.subscribeMqtt(
|
|
53332
54058
|
serverId,
|
|
@@ -53352,6 +54078,7 @@ var WagoService = class {
|
|
|
53352
54078
|
return;
|
|
53353
54079
|
}
|
|
53354
54080
|
this.activeSubscriptionGeneration = generation;
|
|
54081
|
+
retainedStates.forEach((payload, controllerId) => this.diagnostics.ingest(controllerId, "state", payload));
|
|
53355
54082
|
this.unsubscribe();
|
|
53356
54083
|
this.subscriptions.push(...replacements);
|
|
53357
54084
|
}
|
|
@@ -53443,15 +54170,35 @@ var WagoService = class {
|
|
|
53443
54170
|
return;
|
|
53444
54171
|
}
|
|
53445
54172
|
if (heartbeat.hardwareId !== hardwareId) return;
|
|
54173
|
+
let rawHeartbeat;
|
|
54174
|
+
try {
|
|
54175
|
+
rawHeartbeat = JSON.parse(payload.toString("utf8"));
|
|
54176
|
+
} catch {
|
|
54177
|
+
return;
|
|
54178
|
+
}
|
|
54179
|
+
const canonical = canonicalEnvelope(rawHeartbeat, "heartbeat");
|
|
53446
54180
|
const controller = await this.controllers.findOneBy({ hardwareId });
|
|
53447
|
-
if (!controller || controller.trustState !== "claimed" || heartbeat.sequence !== void 0 && heartbeat.sequence < controller.lastSequence)
|
|
54181
|
+
if (!controller || controller.trustState !== "claimed" || !canonical && heartbeat.sequence !== void 0 && heartbeat.sequence < controller.lastSequence)
|
|
53448
54182
|
return;
|
|
53449
54183
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
54184
|
+
const canTrackDiagnostics = this.diagnostics.canTrack(controller.id);
|
|
54185
|
+
const admitted = this.diagnostics.ingest(controller.id, "heartbeat", payload);
|
|
54186
|
+
const heartbeatAt = admitted ? this.diagnostics.read(controller.id).heartbeatAt : typeof rawHeartbeat.timestamp === "string" ? rawHeartbeat.timestamp : void 0;
|
|
54187
|
+
const persistedHeartbeatAt = sourceTime(controller.lastHeartbeatAt);
|
|
54188
|
+
if (canonical && (!validEnvelope(rawHeartbeat, Date.now()) || canTrackDiagnostics && !admitted || !heartbeatAt || persistedHeartbeatAt !== null && sourceTime(heartbeatAt) < persistedHeartbeatAt))
|
|
54189
|
+
return;
|
|
54190
|
+
const metadataChanged = controller.protocolVersion !== heartbeat.protocolVersion || controller.runtimeVersion !== heartbeat.runtimeVersion || controller.capabilities !== JSON.stringify(heartbeat.capabilities) || controller.compatibilityError !== compatibilityError(heartbeat);
|
|
54191
|
+
if (controller.lastHeartbeatAt && freshness(controller.lastSeenAt, Date.now(), 3e4) === "fresh" && !metadataChanged)
|
|
54192
|
+
return;
|
|
53450
54193
|
controller.protocolVersion = heartbeat.protocolVersion;
|
|
53451
54194
|
controller.runtimeVersion = heartbeat.runtimeVersion;
|
|
53452
54195
|
controller.capabilities = JSON.stringify(heartbeat.capabilities);
|
|
53453
|
-
|
|
53454
|
-
|
|
54196
|
+
if (!canonical) {
|
|
54197
|
+
controller.lastSequence = this.diagnostics.read(controller.id).legacyHeartbeatSequence ?? controller.lastSequence;
|
|
54198
|
+
controller.lastHeartbeatAt = now;
|
|
54199
|
+
} else {
|
|
54200
|
+
controller.lastHeartbeatAt = heartbeatAt;
|
|
54201
|
+
}
|
|
53455
54202
|
controller.lastSeenAt = now;
|
|
53456
54203
|
controller.compatibilityError = compatibilityError(heartbeat);
|
|
53457
54204
|
controller.updatedAt = now;
|
|
@@ -53562,7 +54309,8 @@ var WagoService = class {
|
|
|
53562
54309
|
}
|
|
53563
54310
|
connectivity(controller) {
|
|
53564
54311
|
if (controller.trustState === "untrusted") return "untrusted";
|
|
53565
|
-
|
|
54312
|
+
const heartbeatAt = this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt;
|
|
54313
|
+
return freshness(heartbeatAt, Date.now(), STALE_AFTER_MS) === "fresh" ? "online" : "stale";
|
|
53566
54314
|
}
|
|
53567
54315
|
async appliedRevision(controllerId) {
|
|
53568
54316
|
const [revision] = await this.revisions.find({
|
|
@@ -53734,10 +54482,10 @@ var import_node_fs2 = require("node:fs");
|
|
|
53734
54482
|
var import_promises4 = require("node:fs/promises");
|
|
53735
54483
|
var import_node_os2 = require("node:os");
|
|
53736
54484
|
var import_node_path2 = require("node:path");
|
|
53737
|
-
var
|
|
54485
|
+
var import_plugins_backend_sdk12 = require("@attraccess/plugins-backend-sdk");
|
|
53738
54486
|
|
|
53739
54487
|
// backend/wago-commissioning-session.entity.ts
|
|
53740
|
-
var
|
|
54488
|
+
var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
|
|
53741
54489
|
var WagoCommissioningSession = class {
|
|
53742
54490
|
constructor() {
|
|
53743
54491
|
__publicField(this, "id");
|
|
@@ -53769,85 +54517,85 @@ var WagoCommissioningSession = class {
|
|
|
53769
54517
|
}
|
|
53770
54518
|
};
|
|
53771
54519
|
__decorateClass([
|
|
53772
|
-
(0,
|
|
54520
|
+
(0, import_plugins_backend_sdk8.PrimaryGeneratedColumn)({ type: "integer" })
|
|
53773
54521
|
], WagoCommissioningSession.prototype, "id", 2);
|
|
53774
54522
|
__decorateClass([
|
|
53775
|
-
(0,
|
|
54523
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "hardware_id" })
|
|
53776
54524
|
], WagoCommissioningSession.prototype, "hardwareId", 2);
|
|
53777
54525
|
__decorateClass([
|
|
53778
|
-
(0,
|
|
54526
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "mqtt_server_id" })
|
|
53779
54527
|
], WagoCommissioningSession.prototype, "mqttServerId", 2);
|
|
53780
54528
|
__decorateClass([
|
|
53781
|
-
(0,
|
|
54529
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "target_host" })
|
|
53782
54530
|
], WagoCommissioningSession.prototype, "targetHost", 2);
|
|
53783
54531
|
__decorateClass([
|
|
53784
|
-
(0,
|
|
54532
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "host_key_fingerprint" })
|
|
53785
54533
|
], WagoCommissioningSession.prototype, "hostKeyFingerprint", 2);
|
|
53786
54534
|
__decorateClass([
|
|
53787
|
-
(0,
|
|
54535
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "firmware_baseline" })
|
|
53788
54536
|
], WagoCommissioningSession.prototype, "firmwareBaseline", 2);
|
|
53789
54537
|
__decorateClass([
|
|
53790
|
-
(0,
|
|
54538
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "controller_name", nullable: true })
|
|
53791
54539
|
], WagoCommissioningSession.prototype, "controllerName", 2);
|
|
53792
54540
|
__decorateClass([
|
|
53793
|
-
(0,
|
|
54541
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar" })
|
|
53794
54542
|
], WagoCommissioningSession.prototype, "state", 2);
|
|
53795
54543
|
__decorateClass([
|
|
53796
|
-
(0,
|
|
54544
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "enrollment_expires_at", nullable: true })
|
|
53797
54545
|
], WagoCommissioningSession.prototype, "enrollmentExpiresAt", 2);
|
|
53798
54546
|
__decorateClass([
|
|
53799
|
-
(0,
|
|
54547
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "enrollment_id", nullable: true })
|
|
53800
54548
|
], WagoCommissioningSession.prototype, "enrollmentId", 2);
|
|
53801
54549
|
__decorateClass([
|
|
53802
|
-
(0,
|
|
54550
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "pairing_code", nullable: true })
|
|
53803
54551
|
], WagoCommissioningSession.prototype, "pairingCode", 2);
|
|
53804
54552
|
__decorateClass([
|
|
53805
|
-
(0,
|
|
54553
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "delivery_token", nullable: true })
|
|
53806
54554
|
], WagoCommissioningSession.prototype, "deliveryToken", 2);
|
|
53807
54555
|
__decorateClass([
|
|
53808
|
-
(0,
|
|
54556
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "text", name: "initiating_principal", nullable: true })
|
|
53809
54557
|
], WagoCommissioningSession.prototype, "initiatingPrincipal", 2);
|
|
53810
54558
|
__decorateClass([
|
|
53811
|
-
(0,
|
|
54559
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "runtime_artifact_digest", nullable: true })
|
|
53812
54560
|
], WagoCommissioningSession.prototype, "runtimeArtifactDigest", 2);
|
|
53813
54561
|
__decorateClass([
|
|
53814
|
-
(0,
|
|
54562
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "management_controller_id", nullable: true })
|
|
53815
54563
|
], WagoCommissioningSession.prototype, "managementControllerId", 2);
|
|
53816
54564
|
__decorateClass([
|
|
53817
|
-
(0,
|
|
54565
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "docker_provision_token", nullable: true })
|
|
53818
54566
|
], WagoCommissioningSession.prototype, "dockerProvisionToken", 2);
|
|
53819
54567
|
__decorateClass([
|
|
53820
|
-
(0,
|
|
54568
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "docker_provision_state", nullable: true })
|
|
53821
54569
|
], WagoCommissioningSession.prototype, "dockerProvisionState", 2);
|
|
53822
54570
|
__decorateClass([
|
|
53823
|
-
(0,
|
|
54571
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "text", name: "platform_report", nullable: true })
|
|
53824
54572
|
], WagoCommissioningSession.prototype, "platformReport", 2);
|
|
53825
54573
|
__decorateClass([
|
|
53826
|
-
(0,
|
|
54574
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "codesys_state", nullable: true })
|
|
53827
54575
|
], WagoCommissioningSession.prototype, "codesysState", 2);
|
|
53828
54576
|
__decorateClass([
|
|
53829
|
-
(0,
|
|
54577
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "progress_percent", nullable: true })
|
|
53830
54578
|
], WagoCommissioningSession.prototype, "progressPercent", 2);
|
|
53831
54579
|
__decorateClass([
|
|
53832
|
-
(0,
|
|
54580
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "progress_step", nullable: true })
|
|
53833
54581
|
], WagoCommissioningSession.prototype, "progressStep", 2);
|
|
53834
54582
|
__decorateClass([
|
|
53835
|
-
(0,
|
|
54583
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "progress_detail", nullable: true })
|
|
53836
54584
|
], WagoCommissioningSession.prototype, "progressDetail", 2);
|
|
53837
54585
|
__decorateClass([
|
|
53838
|
-
(0,
|
|
54586
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "audit_log" })
|
|
53839
54587
|
], WagoCommissioningSession.prototype, "auditLog", 2);
|
|
53840
54588
|
__decorateClass([
|
|
53841
|
-
(0,
|
|
54589
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "failure_reason", nullable: true })
|
|
53842
54590
|
], WagoCommissioningSession.prototype, "failureReason", 2);
|
|
53843
54591
|
__decorateClass([
|
|
53844
|
-
(0,
|
|
54592
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "created_at" })
|
|
53845
54593
|
], WagoCommissioningSession.prototype, "createdAt", 2);
|
|
53846
54594
|
__decorateClass([
|
|
53847
|
-
(0,
|
|
54595
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "updated_at" })
|
|
53848
54596
|
], WagoCommissioningSession.prototype, "updatedAt", 2);
|
|
53849
54597
|
WagoCommissioningSession = __decorateClass([
|
|
53850
|
-
(0,
|
|
54598
|
+
(0, import_plugins_backend_sdk8.Entity)({ name: "plugin_wago_commissioning_sessions" })
|
|
53851
54599
|
], WagoCommissioningSession);
|
|
53852
54600
|
|
|
53853
54601
|
// backend/wago-commissioning-verification.ts
|
|
@@ -54245,13 +54993,13 @@ async function verifyRuntimeSignature(file, armor, trustedKey = WAGO_RUNTIME_REL
|
|
|
54245
54993
|
if (!match) invalid();
|
|
54246
54994
|
const packet = Buffer.from(match[1].replace(/\s/g, ""), "base64");
|
|
54247
54995
|
if (packet.length < 10 || packet.subarray(0, 6).toString() !== "SSHSIG" || packet.readUInt32BE(6) !== 1) invalid();
|
|
54248
|
-
const
|
|
54249
|
-
const key =
|
|
54250
|
-
const namespace =
|
|
54251
|
-
const reserved =
|
|
54252
|
-
const algorithm2 =
|
|
54253
|
-
const signature = reader(
|
|
54254
|
-
|
|
54996
|
+
const fields2 = reader(packet.subarray(10));
|
|
54997
|
+
const key = fields2.string();
|
|
54998
|
+
const namespace = fields2.string();
|
|
54999
|
+
const reserved = fields2.string();
|
|
55000
|
+
const algorithm2 = fields2.string();
|
|
55001
|
+
const signature = reader(fields2.string());
|
|
55002
|
+
fields2.end();
|
|
54255
55003
|
if (!key.equals(Buffer.from(trustedKey, "base64")) || namespace.toString() !== "attraccess-wago-runtime" || reserved.length)
|
|
54256
55004
|
invalid();
|
|
54257
55005
|
if (!["sha256", "sha512"].includes(algorithm2.toString()) || signature.string().toString() !== "ssh-ed25519")
|
|
@@ -54716,7 +55464,7 @@ WagoRuntimeArtifactsService = __decorateClass([
|
|
|
54716
55464
|
|
|
54717
55465
|
// backend/wago-commissioning-readiness.ts
|
|
54718
55466
|
var import_common5 = require("@nestjs/common");
|
|
54719
|
-
var
|
|
55467
|
+
var import_plugins_backend_sdk9 = require("@attraccess/plugins-backend-sdk");
|
|
54720
55468
|
var WagoCommissioningReadiness = class {
|
|
54721
55469
|
constructor(context) {
|
|
54722
55470
|
__publicField(this, "context", context);
|
|
@@ -54780,14 +55528,14 @@ var WagoCommissioningReadiness = class {
|
|
|
54780
55528
|
};
|
|
54781
55529
|
WagoCommissioningReadiness = __decorateClass([
|
|
54782
55530
|
(0, import_common5.Injectable)(),
|
|
54783
|
-
__decorateParam(0, (0, import_common5.Inject)(
|
|
55531
|
+
__decorateParam(0, (0, import_common5.Inject)(import_plugins_backend_sdk9.PLUGIN_CONTEXT))
|
|
54784
55532
|
], WagoCommissioningReadiness);
|
|
54785
55533
|
|
|
54786
55534
|
// backend/wago-commissioning-lease.ts
|
|
54787
55535
|
var import_node_crypto8 = require("node:crypto");
|
|
54788
55536
|
|
|
54789
55537
|
// backend/wago-commissioning-lease.entity.ts
|
|
54790
|
-
var
|
|
55538
|
+
var import_plugins_backend_sdk10 = require("@attraccess/plugins-backend-sdk");
|
|
54791
55539
|
var WagoCommissioningLeaseEntity = class {
|
|
54792
55540
|
constructor() {
|
|
54793
55541
|
__publicField(this, "fingerprintHash");
|
|
@@ -54798,22 +55546,22 @@ var WagoCommissioningLeaseEntity = class {
|
|
|
54798
55546
|
}
|
|
54799
55547
|
};
|
|
54800
55548
|
__decorateClass([
|
|
54801
|
-
(0,
|
|
55549
|
+
(0, import_plugins_backend_sdk10.PrimaryColumn)({ type: "varchar", name: "fingerprint_hash", length: 64 })
|
|
54802
55550
|
], WagoCommissioningLeaseEntity.prototype, "fingerprintHash", 2);
|
|
54803
55551
|
__decorateClass([
|
|
54804
|
-
(0,
|
|
55552
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "varchar", name: "owner" })
|
|
54805
55553
|
], WagoCommissioningLeaseEntity.prototype, "owner", 2);
|
|
54806
55554
|
__decorateClass([
|
|
54807
|
-
(0,
|
|
55555
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "lease_until" })
|
|
54808
55556
|
], WagoCommissioningLeaseEntity.prototype, "leaseUntil", 2);
|
|
54809
55557
|
__decorateClass([
|
|
54810
|
-
(0,
|
|
55558
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "operation_until" })
|
|
54811
55559
|
], WagoCommissioningLeaseEntity.prototype, "operationUntil", 2);
|
|
54812
55560
|
__decorateClass([
|
|
54813
|
-
(0,
|
|
55561
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "recovery_after" })
|
|
54814
55562
|
], WagoCommissioningLeaseEntity.prototype, "recoveryAfter", 2);
|
|
54815
55563
|
WagoCommissioningLeaseEntity = __decorateClass([
|
|
54816
|
-
(0,
|
|
55564
|
+
(0, import_plugins_backend_sdk10.Entity)({ name: "plugin_wago_commissioning_lease" })
|
|
54817
55565
|
], WagoCommissioningLeaseEntity);
|
|
54818
55566
|
|
|
54819
55567
|
// backend/wago-commissioning-lease.ts
|
|
@@ -54989,7 +55737,7 @@ function createWagoCommissioningLeaseService(context) {
|
|
|
54989
55737
|
}
|
|
54990
55738
|
|
|
54991
55739
|
// backend/wago-management.entity.ts
|
|
54992
|
-
var
|
|
55740
|
+
var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
|
|
54993
55741
|
var WagoManagementEntity = class {
|
|
54994
55742
|
constructor() {
|
|
54995
55743
|
__publicField(this, "controllerId");
|
|
@@ -55000,22 +55748,22 @@ var WagoManagementEntity = class {
|
|
|
55000
55748
|
}
|
|
55001
55749
|
};
|
|
55002
55750
|
__decorateClass([
|
|
55003
|
-
(0,
|
|
55751
|
+
(0, import_plugins_backend_sdk11.PrimaryColumn)({ type: "integer", name: "controller_id" })
|
|
55004
55752
|
], WagoManagementEntity.prototype, "controllerId", 2);
|
|
55005
55753
|
__decorateClass([
|
|
55006
|
-
(0,
|
|
55754
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "text", name: "metadata_json", nullable: true })
|
|
55007
55755
|
], WagoManagementEntity.prototype, "metadataJson", 2);
|
|
55008
55756
|
__decorateClass([
|
|
55009
|
-
(0,
|
|
55757
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "text", name: "encrypted_private_key", nullable: true, select: false })
|
|
55010
55758
|
], WagoManagementEntity.prototype, "encryptedPrivateKey", 2);
|
|
55011
55759
|
__decorateClass([
|
|
55012
|
-
(0,
|
|
55760
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "varchar", name: "lease_owner", nullable: true })
|
|
55013
55761
|
], WagoManagementEntity.prototype, "leaseOwner", 2);
|
|
55014
55762
|
__decorateClass([
|
|
55015
|
-
(0,
|
|
55763
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "bigint", name: "lease_until", default: 0 })
|
|
55016
55764
|
], WagoManagementEntity.prototype, "leaseUntil", 2);
|
|
55017
55765
|
WagoManagementEntity = __decorateClass([
|
|
55018
|
-
(0,
|
|
55766
|
+
(0, import_plugins_backend_sdk11.Entity)({ name: "plugin_wago_management" })
|
|
55019
55767
|
], WagoManagementEntity);
|
|
55020
55768
|
|
|
55021
55769
|
// backend/wago-management-inspection.ts
|
|
@@ -55480,7 +56228,7 @@ var transitionStates = [
|
|
|
55480
56228
|
"recovery_required"
|
|
55481
56229
|
];
|
|
55482
56230
|
var exceptionNames = ["wbm_exposed", "other_services_exposed", "unqualified_privileges"];
|
|
55483
|
-
var
|
|
56231
|
+
var identifier2 = () => (0, import_node_crypto10.randomBytes)(16).toString("hex");
|
|
55484
56232
|
var REVIEW_MS = 3e5;
|
|
55485
56233
|
var LEASE_MS = 30 * 6e4;
|
|
55486
56234
|
var ManagementError = class extends Error {
|
|
@@ -55547,7 +56295,7 @@ var WagoManagementService = class {
|
|
|
55547
56295
|
record3.support = this.adapter.qualify(record3.inspection, input.mode).support;
|
|
55548
56296
|
if (input.mode === "key_only" && (!record3.exceptions.includes("unqualified_privileges") || record3.inspection.wbm !== "not_observed" && !record3.exceptions.includes("wbm_exposed") || record3.inspection.otherManagement !== "not_observed" && !record3.exceptions.includes("other_services_exposed")))
|
|
55549
56297
|
throw new ManagementError("invalid_request");
|
|
55550
|
-
record3.reviewToken =
|
|
56298
|
+
record3.reviewToken = identifier2();
|
|
55551
56299
|
record3.reviewedAt = this.now();
|
|
55552
56300
|
record3.state = "reviewed";
|
|
55553
56301
|
await this.save(record3, owner, guard);
|
|
@@ -55580,7 +56328,7 @@ var WagoManagementService = class {
|
|
|
55580
56328
|
if (this.secrets.decrypt(record2.encryptedPrivateKey) !== key.privateKey) throw new Error();
|
|
55581
56329
|
record2.keyFingerprint = key.fingerprint;
|
|
55582
56330
|
record2.transaction = {
|
|
55583
|
-
id:
|
|
56331
|
+
id: identifier2(),
|
|
55584
56332
|
target: record2.target,
|
|
55585
56333
|
username: input.temporarySsh.username,
|
|
55586
56334
|
deadline: this.now() + 15e4
|
|
@@ -55636,7 +56384,7 @@ var WagoManagementService = class {
|
|
|
55636
56384
|
});
|
|
55637
56385
|
}
|
|
55638
56386
|
async verify(record2, privateKey, guard) {
|
|
55639
|
-
const nonce =
|
|
56387
|
+
const nonce = identifier2();
|
|
55640
56388
|
const tx = this.transaction(record2);
|
|
55641
56389
|
const proof = await this.remote(guard, () => this.adapter.verifyKey(tx, privateKey, nonce));
|
|
55642
56390
|
if (proof.nonce !== nonce || !proof.keyOnly || proof.hostKeyFingerprint !== tx.target.hostKeyFingerprint || proof.keyFingerprint !== record2.keyFingerprint || !Number.isSafeInteger(proof.uid) || proof.uid <= 0 || proof.uid !== this.inspection(record2).uid || !proof.managementOperationSucceeded)
|
|
@@ -55708,7 +56456,7 @@ var WagoManagementService = class {
|
|
|
55708
56456
|
}
|
|
55709
56457
|
async locked(controllerId, fingerprint2, action) {
|
|
55710
56458
|
validId(controllerId);
|
|
55711
|
-
const owner =
|
|
56459
|
+
const owner = identifier2();
|
|
55712
56460
|
let acquired = false;
|
|
55713
56461
|
let mutationStarted = false;
|
|
55714
56462
|
try {
|
|
@@ -57319,7 +58067,7 @@ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
|
|
|
57319
58067
|
};
|
|
57320
58068
|
WagoCommissioningService = __decorateClass([
|
|
57321
58069
|
(0, import_common6.Injectable)(),
|
|
57322
|
-
__decorateParam(0, (0, import_common6.Inject)(
|
|
58070
|
+
__decorateParam(0, (0, import_common6.Inject)(import_plugins_backend_sdk12.PLUGIN_CONTEXT)),
|
|
57323
58071
|
__decorateParam(1, (0, import_common6.Inject)(WagoService)),
|
|
57324
58072
|
__decorateParam(2, (0, import_common6.Optional)()),
|
|
57325
58073
|
__decorateParam(2, (0, import_common6.Inject)(WagoRuntimeArtifactsService)),
|
|
@@ -57754,17 +58502,23 @@ var WagoControllerApi = class {
|
|
|
57754
58502
|
}
|
|
57755
58503
|
previewPreset(id, body) {
|
|
57756
58504
|
if (!body?.application) throw new import_common8.BadRequestException("application is required");
|
|
57757
|
-
return this.wago.previewPreset(id, body.application);
|
|
58505
|
+
return this.wago.previewPreset(id, body.application, body.snapshot);
|
|
57758
58506
|
}
|
|
57759
58507
|
applyPreset(id, body) {
|
|
57760
58508
|
if (!body?.application) throw new import_common8.BadRequestException("application is required");
|
|
57761
|
-
return this.wago.applyPreset(
|
|
58509
|
+
return this.wago.applyPreset(
|
|
58510
|
+
id,
|
|
58511
|
+
body.application,
|
|
58512
|
+
body.selectedPaths ?? [],
|
|
58513
|
+
body.previewedDraftHash ?? "",
|
|
58514
|
+
body.snapshot
|
|
58515
|
+
);
|
|
57762
58516
|
}
|
|
57763
58517
|
saveDraft(id, body) {
|
|
57764
|
-
return this.wago.saveDraft(id, body?.snapshot);
|
|
58518
|
+
return this.wago.saveDraft(id, body?.snapshot, body?.metadata);
|
|
57765
58519
|
}
|
|
57766
|
-
validateDraft(id) {
|
|
57767
|
-
return this.wago.validateDraft(id);
|
|
58520
|
+
validateDraft(id, body) {
|
|
58521
|
+
return this.wago.validateDraft(id, body?.snapshot);
|
|
57768
58522
|
}
|
|
57769
58523
|
reviewDraft(id) {
|
|
57770
58524
|
return this.wago.reviewDraft(id);
|
|
@@ -57772,23 +58526,31 @@ var WagoControllerApi = class {
|
|
|
57772
58526
|
revisions(id, offset, limit) {
|
|
57773
58527
|
return this.wago.revisionsFor(id, Number(offset), Number(limit));
|
|
57774
58528
|
}
|
|
57775
|
-
publishDraft(id, request) {
|
|
58529
|
+
publishDraft(id, request, body) {
|
|
57776
58530
|
return this.audit.run(
|
|
57777
58531
|
wagoAuditPrincipal(request),
|
|
57778
58532
|
id,
|
|
57779
58533
|
"publication",
|
|
57780
|
-
{},
|
|
57781
|
-
() => this.wago.publishDraft(id),
|
|
58534
|
+
body?.force === true ? { force: true } : {},
|
|
58535
|
+
() => this.wago.publishDraft(id, body?.force === true, body?.reviewedHash),
|
|
57782
58536
|
(published) => ({ revision: published.revision })
|
|
57783
58537
|
);
|
|
57784
58538
|
}
|
|
57785
|
-
rollback(id, revision, request) {
|
|
58539
|
+
rollback(id, revision, request, body) {
|
|
57786
58540
|
return this.audit.run(
|
|
57787
58541
|
wagoAuditPrincipal(request),
|
|
57788
58542
|
id,
|
|
57789
58543
|
"rollback",
|
|
57790
|
-
{ sourceRevision: revision },
|
|
57791
|
-
() => this.wago.rollback(
|
|
58544
|
+
body?.force === true ? { sourceRevision: revision, force: true } : { sourceRevision: revision },
|
|
58545
|
+
() => this.wago.rollback(
|
|
58546
|
+
id,
|
|
58547
|
+
revision,
|
|
58548
|
+
body?.force === true,
|
|
58549
|
+
body?.sourceHash,
|
|
58550
|
+
body?.currentHash,
|
|
58551
|
+
body?.draftHash,
|
|
58552
|
+
body?.impactHash
|
|
58553
|
+
),
|
|
57792
58554
|
(published) => ({ revision: published.revision })
|
|
57793
58555
|
);
|
|
57794
58556
|
}
|
|
@@ -57800,61 +58562,61 @@ __decorateClass([
|
|
|
57800
58562
|
(0, import_common8.Get)("controllers")
|
|
57801
58563
|
], WagoControllerApi.prototype, "list", 1);
|
|
57802
58564
|
__decorateClass([
|
|
57803
|
-
(0,
|
|
58565
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57804
58566
|
(0, import_common8.Get)("settings")
|
|
57805
58567
|
], WagoControllerApi.prototype, "settings", 1);
|
|
57806
58568
|
__decorateClass([
|
|
57807
|
-
(0,
|
|
58569
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57808
58570
|
(0, import_common8.Post)("settings"),
|
|
57809
58571
|
__decorateParam(0, (0, import_common8.Body)())
|
|
57810
58572
|
], WagoControllerApi.prototype, "setSettings", 1);
|
|
57811
58573
|
__decorateClass([
|
|
57812
|
-
(0,
|
|
58574
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57813
58575
|
(0, import_common8.Get)("commissioning/support")
|
|
57814
58576
|
], WagoControllerApi.prototype, "commissioningSupport", 1);
|
|
57815
58577
|
__decorateClass([
|
|
57816
|
-
(0,
|
|
58578
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57817
58579
|
(0, import_common8.Get)("commissioning/sessions"),
|
|
57818
58580
|
__decorateParam(0, (0, import_common8.Query)("limit")),
|
|
57819
58581
|
__decorateParam(1, (0, import_common8.Query)("offset"))
|
|
57820
58582
|
], WagoControllerApi.prototype, "commissioningSessions", 1);
|
|
57821
58583
|
__decorateClass([
|
|
57822
|
-
(0,
|
|
58584
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57823
58585
|
(0, import_common8.Post)("commissioning/sessions"),
|
|
57824
58586
|
__decorateParam(0, (0, import_common8.Body)())
|
|
57825
58587
|
], WagoControllerApi.prototype, "createCommissioningSession", 1);
|
|
57826
58588
|
__decorateClass([
|
|
57827
|
-
(0,
|
|
58589
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57828
58590
|
(0, import_common8.Post)("commissioning/sessions/:id/confirm-host-key"),
|
|
57829
58591
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57830
58592
|
__decorateParam(1, (0, import_common8.Body)())
|
|
57831
58593
|
], WagoControllerApi.prototype, "confirmCommissioningHostKey", 1);
|
|
57832
58594
|
__decorateClass([
|
|
57833
|
-
(0,
|
|
58595
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57834
58596
|
(0, import_common8.Post)("commissioning/sessions/:id/deliver"),
|
|
57835
58597
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57836
58598
|
__decorateParam(1, (0, import_common8.Body)()),
|
|
57837
58599
|
__decorateParam(2, (0, import_common8.Req)())
|
|
57838
58600
|
], WagoControllerApi.prototype, "deliverCommissioningSession", 1);
|
|
57839
58601
|
__decorateClass([
|
|
57840
|
-
(0,
|
|
58602
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57841
58603
|
(0, import_common8.Post)("commissioning/sessions/:id/recover"),
|
|
57842
58604
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57843
58605
|
__decorateParam(1, (0, import_common8.Body)()),
|
|
57844
58606
|
__decorateParam(2, (0, import_common8.Req)())
|
|
57845
58607
|
], WagoControllerApi.prototype, "recoverCommissioningSession", 1);
|
|
57846
58608
|
__decorateClass([
|
|
57847
|
-
(0,
|
|
58609
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57848
58610
|
(0, import_common8.Get)("commissioning/sessions/:id/verification"),
|
|
57849
58611
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
57850
58612
|
], WagoControllerApi.prototype, "commissioningVerification", 1);
|
|
57851
58613
|
__decorateClass([
|
|
57852
|
-
(0,
|
|
58614
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57853
58615
|
(0, import_common8.Post)("commissioning/sessions/:id/revoke"),
|
|
57854
58616
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
57855
58617
|
], WagoControllerApi.prototype, "revokeCommissioningSession", 1);
|
|
57856
58618
|
__decorateClass([
|
|
57857
|
-
(0,
|
|
58619
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57858
58620
|
(0, import_common8.Delete)("commissioning/sessions/:id"),
|
|
57859
58621
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
57860
58622
|
], WagoControllerApi.prototype, "removeCommissioningSession", 1);
|
|
@@ -57893,7 +58655,8 @@ __decorateClass([
|
|
|
57893
58655
|
], WagoControllerApi.prototype, "saveDraft", 1);
|
|
57894
58656
|
__decorateClass([
|
|
57895
58657
|
(0, import_common8.Post)("controllers/:id/configuration/validate"),
|
|
57896
|
-
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
58658
|
+
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
58659
|
+
__decorateParam(1, (0, import_common8.Body)())
|
|
57897
58660
|
], WagoControllerApi.prototype, "validateDraft", 1);
|
|
57898
58661
|
__decorateClass([
|
|
57899
58662
|
(0, import_common8.Post)("controllers/:id/configuration/review"),
|
|
@@ -57908,13 +58671,15 @@ __decorateClass([
|
|
|
57908
58671
|
__decorateClass([
|
|
57909
58672
|
(0, import_common8.Post)("controllers/:id/configuration/publish"),
|
|
57910
58673
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57911
|
-
__decorateParam(1, (0, import_common8.Req)())
|
|
58674
|
+
__decorateParam(1, (0, import_common8.Req)()),
|
|
58675
|
+
__decorateParam(2, (0, import_common8.Body)())
|
|
57912
58676
|
], WagoControllerApi.prototype, "publishDraft", 1);
|
|
57913
58677
|
__decorateClass([
|
|
57914
58678
|
(0, import_common8.Post)("controllers/:id/configuration/rollback/:revision"),
|
|
57915
58679
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57916
58680
|
__decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe)),
|
|
57917
|
-
__decorateParam(2, (0, import_common8.Req)())
|
|
58681
|
+
__decorateParam(2, (0, import_common8.Req)()),
|
|
58682
|
+
__decorateParam(3, (0, import_common8.Body)())
|
|
57918
58683
|
], WagoControllerApi.prototype, "rollback", 1);
|
|
57919
58684
|
__decorateClass([
|
|
57920
58685
|
(0, import_common8.Get)("controllers/:id/configuration/revisions/:revision/preview"),
|
|
@@ -57922,7 +58687,7 @@ __decorateClass([
|
|
|
57922
58687
|
__decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe))
|
|
57923
58688
|
], WagoControllerApi.prototype, "previewRevision", 1);
|
|
57924
58689
|
WagoControllerApi = __decorateClass([
|
|
57925
|
-
(0,
|
|
58690
|
+
(0, import_plugins_backend_sdk13.Auth)("resources.update"),
|
|
57926
58691
|
(0, import_common8.Controller)("wago"),
|
|
57927
58692
|
__decorateParam(0, (0, import_common8.Inject)(WagoService)),
|
|
57928
58693
|
__decorateParam(1, (0, import_common8.Inject)(WagoCommissioningService)),
|
|
@@ -57931,7 +58696,7 @@ WagoControllerApi = __decorateClass([
|
|
|
57931
58696
|
|
|
57932
58697
|
// backend/wago-artifacts.controller.ts
|
|
57933
58698
|
var import_common9 = require("@nestjs/common");
|
|
57934
|
-
var
|
|
58699
|
+
var import_plugins_backend_sdk14 = require("@attraccess/plugins-backend-sdk");
|
|
57935
58700
|
var import_platform_express = __toESM(require_platform_express());
|
|
57936
58701
|
var import_promises5 = require("node:fs/promises");
|
|
57937
58702
|
var import_node_path3 = require("node:path");
|
|
@@ -58149,7 +58914,7 @@ __decorateClass([
|
|
|
58149
58914
|
__decorateParam(0, (0, import_common9.UploadedFiles)())
|
|
58150
58915
|
], WagoArtifactsController.prototype, "import", 1);
|
|
58151
58916
|
WagoArtifactsController = __decorateClass([
|
|
58152
|
-
(0,
|
|
58917
|
+
(0, import_plugins_backend_sdk14.Auth)("system.settings.manage"),
|
|
58153
58918
|
(0, import_common9.Controller)("wago/runtime-artifacts"),
|
|
58154
58919
|
__decorateParam(0, (0, import_common9.Inject)(WagoRuntimeArtifactsService))
|
|
58155
58920
|
], WagoArtifactsController);
|
|
@@ -58619,6 +59384,167 @@ WagoFlowService = __decorateClass([
|
|
|
58619
59384
|
__decorateParam(0, (0, import_common10.Inject)(PLUGIN_CONTEXT4))
|
|
58620
59385
|
], WagoFlowService);
|
|
58621
59386
|
|
|
59387
|
+
// backend/diagnostics.controller.ts
|
|
59388
|
+
var import_common12 = require("@nestjs/common");
|
|
59389
|
+
var import_plugins_backend_sdk16 = require("@attraccess/plugins-backend-sdk");
|
|
59390
|
+
|
|
59391
|
+
// backend/diagnostics.service.ts
|
|
59392
|
+
var import_common11 = require("@nestjs/common");
|
|
59393
|
+
var import_plugins_backend_sdk15 = require("@attraccess/plugins-backend-sdk");
|
|
59394
|
+
function own(values, key) {
|
|
59395
|
+
return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
|
|
59396
|
+
}
|
|
59397
|
+
function metadataFromProvenance(provenance) {
|
|
59398
|
+
if (!provenance) return { names: {}, presets: [] };
|
|
59399
|
+
try {
|
|
59400
|
+
return editorMetadata(JSON.parse(provenance).editor);
|
|
59401
|
+
} catch {
|
|
59402
|
+
return { names: {}, presets: [] };
|
|
59403
|
+
}
|
|
59404
|
+
}
|
|
59405
|
+
function diagnosticReferences(nodes, channelIds, revision, capabilities) {
|
|
59406
|
+
return nodes.map((node) => {
|
|
59407
|
+
const channelId = typeof node.data.channelId === "string" ? node.data.channelId : "";
|
|
59408
|
+
const control = node.type === "plugin.wago.command";
|
|
59409
|
+
return {
|
|
59410
|
+
nodeId: node.id,
|
|
59411
|
+
resourceId: node.resourceId,
|
|
59412
|
+
channelId,
|
|
59413
|
+
control,
|
|
59414
|
+
href: `/resources/${node.resourceId}/flows`,
|
|
59415
|
+
invalid: !channelIds.includes(channelId) || control && (node.data.expectedConfigurationRevision !== revision || capabilities && (!own(capabilities, channelId)?.includes("output") || node.data.action === "pulse" && !own(capabilities, channelId)?.includes("pulse"))),
|
|
59416
|
+
conflict: control && nodes.some(
|
|
59417
|
+
(other) => other.type === "plugin.wago.command" && other.resourceId !== node.resourceId && other.data.channelId === channelId
|
|
59418
|
+
)
|
|
59419
|
+
};
|
|
59420
|
+
});
|
|
59421
|
+
}
|
|
59422
|
+
var WagoDiagnosticsService = class {
|
|
59423
|
+
constructor(context, wago) {
|
|
59424
|
+
__publicField(this, "context", context);
|
|
59425
|
+
__publicField(this, "wago", wago);
|
|
59426
|
+
}
|
|
59427
|
+
async get(controllerId) {
|
|
59428
|
+
const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
|
|
59429
|
+
if (!controller) throw new import_common11.NotFoundException("WAGO controller not found");
|
|
59430
|
+
const [draft, latest, applied, nodes] = await Promise.all([
|
|
59431
|
+
this.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId }),
|
|
59432
|
+
this.context.getRepository(WagoConfigurationRevision).findOne({ where: { controllerId }, order: { revision: "DESC" } }),
|
|
59433
|
+
this.context.getRepository(WagoConfigurationRevision).findOne({ where: { controllerId, state: "applied" }, order: { revision: "DESC" } }),
|
|
59434
|
+
this.context.dataSource.getRepository(import_plugins_backend_sdk15.ResourceFlowNode).createQueryBuilder("node").where("node.type LIKE :type", { type: "plugin.wago.%" }).andWhere("node.data ->> 'controllerId' = :controllerId", { controllerId }).take(1001).getMany()
|
|
59435
|
+
]);
|
|
59436
|
+
const runtime = this.wago.diagnostics.read(controllerId);
|
|
59437
|
+
const snapshot = latest ? JSON.parse(latest.snapshot) : null;
|
|
59438
|
+
const appliedSnapshot = applied ? JSON.parse(applied.snapshot) : null;
|
|
59439
|
+
const heartbeatAt = runtime.heartbeatAt ?? controller.lastHeartbeatAt;
|
|
59440
|
+
const heartbeatFreshness = freshness(heartbeatAt);
|
|
59441
|
+
const revisionMismatch = !!latest && (applied?.revision !== latest.revision || runtime.revision !== latest.revision || runtime.activeStream !== void 0 && runtime.contentHash !== latest.contentHash);
|
|
59442
|
+
const connected = runtime.connected === false ? false : heartbeatFreshness === "fresh" || freshness(runtime.stateSourceAt) === "fresh";
|
|
59443
|
+
const references = diagnosticReferences(
|
|
59444
|
+
nodes.slice(0, 1e3),
|
|
59445
|
+
appliedSnapshot?.logicalChannels.map((channel) => channel.id) ?? [],
|
|
59446
|
+
applied?.revision ?? null,
|
|
59447
|
+
Object.fromEntries(appliedSnapshot?.logicalChannels.map((channel) => [channel.id, channel.capabilities]) ?? [])
|
|
59448
|
+
);
|
|
59449
|
+
const validationErrors = draft ? validateSnapshot(JSON.parse(draft.snapshot)) : [];
|
|
59450
|
+
return {
|
|
59451
|
+
controllerId,
|
|
59452
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59453
|
+
name: controller.name ?? controller.hardwareId,
|
|
59454
|
+
connectivity: controller.trustState !== "claimed" ? "untrusted" : runtime.connected === false ? "disconnected" : connected ? "online" : "stale",
|
|
59455
|
+
heartbeatAt,
|
|
59456
|
+
heartbeatFreshness,
|
|
59457
|
+
runtimeVersion: controller.runtimeVersion,
|
|
59458
|
+
protocolVersion: controller.protocolVersion,
|
|
59459
|
+
capabilities: JSON.parse(controller.capabilities).slice(0, 64),
|
|
59460
|
+
incompatible: !!controller.compatibilityError,
|
|
59461
|
+
sequenceGaps: runtime.activeStream ? runtime.sequenceGaps : null,
|
|
59462
|
+
activeStream: runtime.activeStream ?? null,
|
|
59463
|
+
trackingExhausted: runtime.trackingExhausted,
|
|
59464
|
+
stateConnected: runtime.connected ?? null,
|
|
59465
|
+
stateHardwareAvailable: runtime.hardwareAvailable ?? null,
|
|
59466
|
+
stateSourceAt: runtime.stateSourceAt ?? null,
|
|
59467
|
+
sequenceExplanation: runtime.activeStream ? "Gaps are scoped to boot UUID and message category; duplicates and retired streams are ignored." : "Legacy payloads have no source envelope; sequence gaps and source freshness are unavailable.",
|
|
59468
|
+
configuration: {
|
|
59469
|
+
draftUpdatedAt: draft?.updatedAt ?? null,
|
|
59470
|
+
draftChanged: !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash(metadataFromProvenance(draft.presetProvenance)) !== configurationHash(metadataFromProvenance(latest.presetProvenance))),
|
|
59471
|
+
validationErrorCount: validationErrors.length,
|
|
59472
|
+
// Codes originate in our validator. Omit messages and dynamic paths, which can include arbitrary draft values.
|
|
59473
|
+
validationCodes: [...new Set(validationErrors.map((error) => error.code))].slice(0, 50),
|
|
59474
|
+
validationErrors: safeValidationSummaries(validationErrors),
|
|
59475
|
+
rejectionErrors: latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [],
|
|
59476
|
+
publishedRevision: latest?.revision ?? null,
|
|
59477
|
+
publishedState: latest?.state ?? null,
|
|
59478
|
+
appliedRevision: applied?.revision ?? null,
|
|
59479
|
+
reportedRevision: runtime.revision ?? null,
|
|
59480
|
+
revisionMismatch,
|
|
59481
|
+
rejected: latest?.state === "rejected"
|
|
59482
|
+
},
|
|
59483
|
+
hardwareReadiness: "unknown",
|
|
59484
|
+
hardwareReadinessReason: "Reported hardware availability is shown when supplied; it does not prove physical I/O readiness. Applied configuration and cached output state are not physical proof.",
|
|
59485
|
+
channels: (snapshot?.logicalChannels ?? []).slice(0, 256).map((channel) => {
|
|
59486
|
+
const values = [
|
|
59487
|
+
channel.capabilities.includes("input") ? own(runtime.inputs, channel.id) : void 0,
|
|
59488
|
+
channel.capabilities.includes("output") ? own(runtime.outputs, channel.id) : void 0,
|
|
59489
|
+
channel.capabilities.includes("measurement") ? own(runtime.measurements, channel.id) : void 0,
|
|
59490
|
+
channel.capabilities.includes("measurement") ? own(runtime.cumulativeMeasurements, channel.id) : void 0
|
|
59491
|
+
].filter((value) => value !== void 0);
|
|
59492
|
+
const samples = values.map((value) => {
|
|
59493
|
+
const sourceFreshness = freshness(value.sourceAt);
|
|
59494
|
+
const availabilityReason = controller.trustState !== "claimed" ? "untrusted" : controller.compatibilityError ? "incompatible-runtime" : runtime.trackingExhausted ? "stream-tracking-exhausted" : runtime.connected !== true || !connected ? "disconnected-or-unknown" : runtime.hardwareAvailable === false ? "hardware-unavailable" : revisionMismatch || !applied ? "configuration-mismatch" : own(runtime.faults, channel.id) ? "recent-fault" : freshness(runtime.stateSourceAt) !== "fresh" ? "state-source-unavailable-or-stale" : value.streamId !== runtime.activeStream ? "old-or-legacy-stream" : sourceFreshness !== "fresh" ? `source-${sourceFreshness}` : "current";
|
|
59495
|
+
return { ...value, sourceFreshness, current: availabilityReason === "current", availabilityReason };
|
|
59496
|
+
});
|
|
59497
|
+
return {
|
|
59498
|
+
id: channel.id,
|
|
59499
|
+
profile: channel.profile,
|
|
59500
|
+
capabilities: channel.capabilities,
|
|
59501
|
+
disconnectPolicy: channel.disconnectPolicy,
|
|
59502
|
+
safeState: channel.capabilities.includes("output") ? "off (runtime default)" : "not applicable",
|
|
59503
|
+
samples,
|
|
59504
|
+
current: samples.length > 0 && samples.every((value) => value.current),
|
|
59505
|
+
fault: own(runtime.faults, channel.id) ?? null,
|
|
59506
|
+
acknowledgement: own(runtime.acknowledgements, channel.id) ?? null
|
|
59507
|
+
};
|
|
59508
|
+
}),
|
|
59509
|
+
faults: Object.entries(runtime.faults).map(([channelId, fault]) => ({ channelId, ...fault })),
|
|
59510
|
+
references,
|
|
59511
|
+
referencesTruncated: nodes.length > 1e3,
|
|
59512
|
+
events: runtime.events,
|
|
59513
|
+
limitations: [
|
|
59514
|
+
"Hardware readiness remains unknown. Legacy samples have no source time and are never current.",
|
|
59515
|
+
"Diagnostics retain at most 256 channels, 50 recent events and 15 minutes in this process.",
|
|
59516
|
+
"Resource-page slots are not integrated. Flow links open the affected resource flow; node IDs identify the exact nodes.",
|
|
59517
|
+
"The MQTT SDK exposes neither retained-message flags nor broker connection events. Source age is bounded to 90 seconds; immediate broker-disconnect detection is unavailable.",
|
|
59518
|
+
"Stream tracking keeps at most 16 retired boot UUIDs per controller and six category counters, and fails closed when exhausted. Replayed source times never become receipt times."
|
|
59519
|
+
]
|
|
59520
|
+
};
|
|
59521
|
+
}
|
|
59522
|
+
};
|
|
59523
|
+
WagoDiagnosticsService = __decorateClass([
|
|
59524
|
+
(0, import_common11.Injectable)(),
|
|
59525
|
+
__decorateParam(0, (0, import_common11.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
|
|
59526
|
+
__decorateParam(1, (0, import_common11.Inject)(WagoService))
|
|
59527
|
+
], WagoDiagnosticsService);
|
|
59528
|
+
|
|
59529
|
+
// backend/diagnostics.controller.ts
|
|
59530
|
+
var WagoDiagnosticsController = class {
|
|
59531
|
+
constructor(diagnostics) {
|
|
59532
|
+
__publicField(this, "diagnostics", diagnostics);
|
|
59533
|
+
}
|
|
59534
|
+
get(id) {
|
|
59535
|
+
return this.diagnostics.get(id);
|
|
59536
|
+
}
|
|
59537
|
+
};
|
|
59538
|
+
__decorateClass([
|
|
59539
|
+
(0, import_common12.Get)("controllers/:id/diagnostics"),
|
|
59540
|
+
__decorateParam(0, (0, import_common12.Param)("id", import_common12.ParseIntPipe))
|
|
59541
|
+
], WagoDiagnosticsController.prototype, "get", 1);
|
|
59542
|
+
WagoDiagnosticsController = __decorateClass([
|
|
59543
|
+
(0, import_plugins_backend_sdk16.Auth)("resources.update"),
|
|
59544
|
+
(0, import_common12.Controller)("wago"),
|
|
59545
|
+
__decorateParam(0, (0, import_common12.Inject)(WagoDiagnosticsService))
|
|
59546
|
+
], WagoDiagnosticsController);
|
|
59547
|
+
|
|
58622
59548
|
// backend/plugin.ts
|
|
58623
59549
|
var PLUGIN_CONTEXT5 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
|
|
58624
59550
|
var WagoPluginModule = class {
|
|
@@ -58675,7 +59601,7 @@ var plugin = {
|
|
|
58675
59601
|
flowService = new WagoFlowService(context);
|
|
58676
59602
|
return {
|
|
58677
59603
|
module: WagoPluginModule,
|
|
58678
|
-
controllers: [WagoControllerApi, WagoArtifactsController],
|
|
59604
|
+
controllers: [WagoControllerApi, WagoArtifactsController, WagoDiagnosticsController],
|
|
58679
59605
|
providers: [
|
|
58680
59606
|
{ provide: PLUGIN_CONTEXT5, useValue: context },
|
|
58681
59607
|
WagoService,
|
|
@@ -58683,6 +59609,7 @@ var plugin = {
|
|
|
58683
59609
|
WagoArtifactUploadInterceptor,
|
|
58684
59610
|
WagoCommissioningReadiness,
|
|
58685
59611
|
WagoCommissioningService,
|
|
59612
|
+
WagoDiagnosticsService,
|
|
58686
59613
|
{ provide: WagoFlowService, useValue: flowService }
|
|
58687
59614
|
]
|
|
58688
59615
|
};
|