@attraccess/plugin-wago 0.1.0-nightly.953.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 fields = !Array.isArray(field2) ? parse(String(field2)) : field2;
665
- for (var j = 0; j < fields.length; j++) {
666
- if (!FIELD_NAME_REGEXP.test(fields[j])) {
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 (fields.indexOf("*") !== -1 || vals.indexOf("*") !== -1) {
675
+ if (fields2.indexOf("*") !== -1 || vals.indexOf("*") !== -1) {
676
676
  return "*";
677
677
  }
678
- for (var i = 0; i < fields.length; i++) {
679
- var fld = fields[i].toLowerCase();
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 + ", " + fields[i] : fields[i];
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 codes = require_codes();
2095
+ var codes2 = require_codes();
2096
2096
  module2.exports = status;
2097
- status.message = codes;
2098
- status.code = createMessageToStatusCodeMap(codes);
2099
- status.codes = createStatusCodeList(codes);
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(codes2) {
2119
+ function createMessageToStatusCodeMap(codes3) {
2120
2120
  var map = {};
2121
- Object.keys(codes2).forEach(function forEachCode(code) {
2122
- var message = codes2[code];
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(codes2) {
2129
- return Object.keys(codes2).map(function mapCode(code) {
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, codes, HttpError) {
2362
- codes.forEach(function forEachCode(code) {
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 fields = 0;
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 (fields === fieldsLimit) {
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
- ++fields;
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 codes = {};
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
- codes[code] = NodeError;
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 = 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(fields, fileStrategy) {
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
- fields.forEach(function(field2) {
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(fields) {
41771
- return this._makeMiddleware(fields, "OBJECT");
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 import_plugins_backend_sdk12 = require("@attraccess/plugins-backend-sdk");
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/wago-configuration-revision.entity.ts
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, import_plugins_backend_sdk5.PrimaryGeneratedColumn)({ type: "integer" })
52605
+ (0, import_plugins_backend_sdk6.PrimaryGeneratedColumn)({ type: "integer" })
52402
52606
  ], WagoConfigurationRevision.prototype, "id", 2);
52403
52607
  __decorateClass([
52404
- (0, import_plugins_backend_sdk5.Column)({ type: "integer", name: "controller_id" })
52608
+ (0, import_plugins_backend_sdk6.Column)({ type: "integer", name: "controller_id" })
52405
52609
  ], WagoConfigurationRevision.prototype, "controllerId", 2);
52406
52610
  __decorateClass([
52407
- (0, import_plugins_backend_sdk5.Column)({ type: "integer" })
52611
+ (0, import_plugins_backend_sdk6.Column)({ type: "integer" })
52408
52612
  ], WagoConfigurationRevision.prototype, "revision", 2);
52409
52613
  __decorateClass([
52410
- (0, import_plugins_backend_sdk5.Column)({ type: "text" })
52614
+ (0, import_plugins_backend_sdk6.Column)({ type: "text" })
52411
52615
  ], WagoConfigurationRevision.prototype, "snapshot", 2);
52412
52616
  __decorateClass([
52413
- (0, import_plugins_backend_sdk5.Column)({ type: "varchar", name: "content_hash" })
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, import_plugins_backend_sdk5.Column)({ type: "varchar" })
52623
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar" })
52417
52624
  ], WagoConfigurationRevision.prototype, "state", 2);
52418
52625
  __decorateClass([
52419
- (0, import_plugins_backend_sdk5.Column)({ type: "text", name: "rejection_errors", nullable: true })
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, import_plugins_backend_sdk5.Column)({ type: "varchar", name: "published_at" })
52629
+ (0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "published_at" })
52423
52630
  ], WagoConfigurationRevision.prototype, "publishedAt", 2);
52424
52631
  __decorateClass([
52425
- (0, import_plugins_backend_sdk5.Column)({ type: "varchar", name: "reported_at", nullable: true })
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, import_plugins_backend_sdk5.Entity)({ name: "plugin_wago_configuration_revisions" })
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 import_plugins_backend_sdk6 = require("@attraccess/plugins-backend-sdk");
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 {
@@ -52457,7 +52664,8 @@ var WagoCommandHandler = class {
52457
52664
  const draft = await this.dependencies.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId });
52458
52665
  try {
52459
52666
  const draftMatchesAppliedRevision = typeof draft?.snapshot === "string" && configurationHash(JSON.parse(draft.snapshot)) === revision?.contentHash;
52460
- const storedNames = draftMatchesAppliedRevision && JSON.parse(draft?.presetProvenance ?? "null")?.editor?.names;
52667
+ const provenance = revision?.presetProvenance ?? (draftMatchesAppliedRevision ? draft?.presetProvenance : null);
52668
+ const storedNames = JSON.parse(provenance ?? "null")?.editor?.names;
52461
52669
  if (storedNames && typeof storedNames === "object" && !Array.isArray(storedNames)) names = storedNames;
52462
52670
  } catch {
52463
52671
  }
@@ -52601,6 +52809,7 @@ var WagoCommandHandler = class {
52601
52809
  throw new WagoCommandError(`WAGO controller ${controllerId} has no MQTT server`, "transport-dispatch");
52602
52810
  const settings = await this.dependencies.getSettings();
52603
52811
  const id = (0, import_node_crypto2.randomUUID)();
52812
+ this.dependencies.onCommand?.(controllerId, channelId, id);
52604
52813
  const command = JSON.stringify({
52605
52814
  id,
52606
52815
  expiresAt: new Date(Date.now() + acknowledgementTimeoutSeconds * 1e3).toISOString(),
@@ -52618,6 +52827,7 @@ var WagoCommandHandler = class {
52618
52827
  { qos: 1, retain: false }
52619
52828
  );
52620
52829
  } catch (error) {
52830
+ this.dependencies.onCommandFailure?.(id, "dispatch-failed");
52621
52831
  const dispatchError = new WagoCommandError(
52622
52832
  `Failed to publish WAGO command: ${String(error)}`,
52623
52833
  "transport-dispatch"
@@ -52657,7 +52867,7 @@ var WagoCommandHandler = class {
52657
52867
  }
52658
52868
  }
52659
52869
  async references(controllerId, channelId, resourceId) {
52660
- const nodes = await this.dependencies.context.dataSource.getRepository(import_plugins_backend_sdk6.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();
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();
52661
52871
  return nodes.map((node) => `resource ${node.resourceId} / node ${node.id}`);
52662
52872
  }
52663
52873
  parse(config) {
@@ -52739,6 +52949,8 @@ var WagoCommandHandler = class {
52739
52949
  pending.resolve();
52740
52950
  }
52741
52951
  reject(id, error) {
52952
+ if (error instanceof WagoCommandError && error.kind === "acknowledgement-timeout")
52953
+ this.dependencies.onCommandFailure?.(id, "timeout");
52742
52954
  const pending = this.pending.get(id);
52743
52955
  if (!pending) return;
52744
52956
  this.pending.delete(id);
@@ -52757,6 +52969,349 @@ function positiveInteger(value) {
52757
52969
  return Number.isSafeInteger(value) && value > 0 ? value : void 0;
52758
52970
  }
52759
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
+
52760
53315
  // backend/wago.service.ts
52761
53316
  var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
52762
53317
  var STALE_AFTER_MS = 9e4;
@@ -52771,6 +53326,7 @@ var MqttSubscriptionError = class extends Error {
52771
53326
  var WagoService = class {
52772
53327
  constructor(context) {
52773
53328
  __publicField(this, "context", context);
53329
+ __publicField(this, "diagnostics", new WagoDiagnosticsStore());
52774
53330
  __publicField(this, "controllers");
52775
53331
  __publicField(this, "settings");
52776
53332
  __publicField(this, "enrollments");
@@ -52788,7 +53344,9 @@ var WagoService = class {
52788
53344
  controllers: () => this.controllers,
52789
53345
  claimedController: (id) => this.claimedController(id),
52790
53346
  getSettings: () => this.getSettings(),
52791
- 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)
52792
53350
  }));
52793
53351
  __publicField(this, "claimConfigurationLock", Promise.resolve());
52794
53352
  __publicField(this, "subscriptionRebuild", Promise.resolve());
@@ -52852,7 +53410,7 @@ var WagoService = class {
52852
53410
  runtimeVersion: controller.runtimeVersion,
52853
53411
  capabilities: controller.capabilities,
52854
53412
  lastSequence: controller.lastSequence,
52855
- lastHeartbeatAt: controller.lastHeartbeatAt,
53413
+ lastHeartbeatAt: this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt,
52856
53414
  lastSeenAt: controller.lastSeenAt,
52857
53415
  compatibilityError: controller.compatibilityError,
52858
53416
  createdAt: controller.createdAt,
@@ -52898,13 +53456,40 @@ var WagoService = class {
52898
53456
  await this.claimedController(controllerId);
52899
53457
  return this.drafts.findOneBy({ controllerId });
52900
53458
  }
52901
- async saveDraft(controllerId, snapshot) {
52902
- return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot));
53459
+ async saveDraft(controllerId, snapshot, metadata) {
53460
+ return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot, metadata));
53461
+ }
53462
+ presets() {
53463
+ return WAGO_PRESETS;
52903
53464
  }
52904
- async validateDraft(controllerId) {
53465
+ async previewPreset(controllerId, application, snapshot) {
52905
53466
  const draft = await this.getDraft(controllerId);
52906
- if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
52907
- const errors = validateSnapshot(JSON.parse(draft.snapshot));
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);
52908
53493
  return { valid: errors.length === 0, errors };
52909
53494
  }
52910
53495
  async revisionsFor(controllerId, offset = 0, limit = 20) {
@@ -52922,7 +53507,8 @@ var WagoService = class {
52922
53507
  "state",
52923
53508
  "rejectionErrors",
52924
53509
  "publishedAt",
52925
- "reportedAt"
53510
+ "reportedAt",
53511
+ "presetProvenance"
52926
53512
  ],
52927
53513
  skip: pageOffset,
52928
53514
  take: pageLimit
@@ -52932,35 +53518,93 @@ var WagoService = class {
52932
53518
  async reviewDraft(controllerId) {
52933
53519
  return this.withConfigurationLock(controllerId, () => this.reviewDraftWhileLocked(controllerId));
52934
53520
  }
52935
- async publishDraft(controllerId) {
52936
- return this.withConfigurationLock(controllerId, () => this.publishDraftWhileLocked(controllerId));
53521
+ async publishDraft(controllerId, force = false, reviewedHash) {
53522
+ return this.withConfigurationLock(
53523
+ controllerId,
53524
+ () => this.publishDraftWhileLocked(controllerId, force, reviewedHash)
53525
+ );
52937
53526
  }
52938
- async rollback(controllerId, revision) {
53527
+ async rollback(controllerId, revision, force = false, sourceHash, currentHash, draftHash, impactHash) {
52939
53528
  return this.withConfigurationLock(controllerId, async () => {
53529
+ await this.claimedController(controllerId);
52940
53530
  const source = await this.revisions.findOneBy({ controllerId, revision });
52941
53531
  if (!source) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
52942
- await this.saveDraftWhileLocked(controllerId, JSON.parse(source.snapshot));
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
+ );
52943
53545
  await this.reviewDraftWhileLocked(controllerId);
52944
- return this.publishDraftWhileLocked(controllerId);
53546
+ return this.publishDraftWhileLocked(controllerId, force);
52945
53547
  });
52946
53548
  }
52947
53549
  async previewRevision(controllerId, revision) {
52948
- await this.claimedController(controllerId);
52949
- const selected = await this.revisions.findOneBy({ controllerId, revision });
52950
- if (!selected) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
52951
- const [current] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
52952
- return {
52953
- revision: selected,
52954
- current: current ?? null,
52955
- diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, JSON.parse(selected.snapshot))
52956
- };
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;
53581
+ }
53582
+ reviewIdentity(draft, impacts) {
53583
+ return configurationHash({ draft: this.draftIdentity(draft), impacts });
52957
53584
  }
52958
- async saveDraftWhileLocked(controllerId, snapshot) {
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) {
52959
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
+ }
52960
53603
  const serialized = canonicalSnapshot(snapshot);
52961
53604
  const existing = await this.drafts.findOneBy({ controllerId });
52962
53605
  const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, updatedAt: "" });
52963
53606
  draft.snapshot = serialized;
53607
+ if (provenance !== void 0) draft.presetProvenance = provenance;
52964
53608
  draft.reviewedHash = null;
52965
53609
  draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
52966
53610
  return this.drafts.save(draft);
@@ -52970,28 +53614,66 @@ var WagoService = class {
52970
53614
  const draft = await this.drafts.findOneBy({ controllerId });
52971
53615
  if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
52972
53616
  const previous = await this.latestRevision(controllerId);
52973
- draft.reviewedHash = configurationHash(JSON.parse(draft.snapshot));
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);
52974
53624
  await this.drafts.save(draft);
52975
53625
  const diff = configurationDiff(previous ? JSON.parse(previous.snapshot) : null, JSON.parse(draft.snapshot));
52976
- return { draft, previous, changed: diff.length > 0, diff };
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
+ };
52977
53638
  }
52978
- async publishDraftWhileLocked(controllerId) {
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) {
52979
53656
  const controller = await this.claimedController(controllerId);
52980
53657
  const draft = await this.drafts.findOneBy({ controllerId });
52981
53658
  if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
52982
- const validation = validateSnapshot(JSON.parse(draft.snapshot));
53659
+ const validation = validateEditorSnapshot(JSON.parse(draft.snapshot));
52983
53660
  if (validation.length)
52984
53661
  throw new import_common.ConflictException({ message: "configuration draft is invalid", errors: validation });
52985
53662
  const contentHash = configurationHash(JSON.parse(draft.snapshot));
52986
- if (draft.reviewedHash !== contentHash)
52987
- throw new import_common.ConflictException("review the current configuration draft before publishing it");
52988
53663
  const previous = await this.latestRevision(controllerId);
52989
- if (previous?.state === "pending" && previous.contentHash === contentHash)
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))
52990
53671
  return this.publishRevision(controller, previous);
52991
53672
  const revision = this.revisions.create({
52992
53673
  controllerId,
52993
53674
  revision: (previous?.revision ?? 0) + 1,
52994
53675
  snapshot: draft.snapshot,
53676
+ presetProvenance: draft.presetProvenance ?? null,
52995
53677
  contentHash,
52996
53678
  state: "pending",
52997
53679
  rejectionErrors: null,
@@ -53307,6 +53989,7 @@ var WagoService = class {
53307
53989
  });
53308
53990
  const generation = this.activeSubscriptionGeneration + 1;
53309
53991
  const replacements = [];
53992
+ const retainedStates = /* @__PURE__ */ new Map();
53310
53993
  try {
53311
53994
  for (const serverId of serverIds) {
53312
53995
  replacements.push(
@@ -53333,7 +54016,10 @@ var WagoService = class {
53333
54016
  if (!this.isActiveSubscriptionGeneration(generation)) return;
53334
54017
  const hardwareId = configurationReportedHardwareId(settings.operationalPrefix, message.topic);
53335
54018
  const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
53336
- if (controller) this.enqueueConfigurationReport(controller.id, message.payload);
54019
+ if (controller) {
54020
+ this.diagnostics.ingest(controller.id, "configuration/reported", message.payload);
54021
+ this.enqueueConfigurationReport(controller.id, message.payload);
54022
+ }
53337
54023
  }
53338
54024
  )
53339
54025
  );
@@ -53342,7 +54028,10 @@ var WagoService = class {
53342
54028
  if (!this.isActiveSubscriptionGeneration(generation)) return;
53343
54029
  const hardwareId = acknowledgementHardwareId(settings.operationalPrefix, message.topic);
53344
54030
  const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
53345
- if (controller) this.onCommandAcknowledgement(controller.id, message.payload);
54031
+ if (controller) {
54032
+ this.diagnostics.ingest(controller.id, "acknowledgements", message.payload);
54033
+ this.onCommandAcknowledgement(controller.id, message.payload);
54034
+ }
53346
54035
  })
53347
54036
  );
53348
54037
  if (this.destroyed) {
@@ -53350,6 +54039,20 @@ var WagoService = class {
53350
54039
  return;
53351
54040
  }
53352
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
+ }
53353
54056
  replacements.push(
53354
54057
  await this.subscribeMqtt(
53355
54058
  serverId,
@@ -53375,6 +54078,7 @@ var WagoService = class {
53375
54078
  return;
53376
54079
  }
53377
54080
  this.activeSubscriptionGeneration = generation;
54081
+ retainedStates.forEach((payload, controllerId) => this.diagnostics.ingest(controllerId, "state", payload));
53378
54082
  this.unsubscribe();
53379
54083
  this.subscriptions.push(...replacements);
53380
54084
  }
@@ -53466,15 +54170,35 @@ var WagoService = class {
53466
54170
  return;
53467
54171
  }
53468
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");
53469
54180
  const controller = await this.controllers.findOneBy({ hardwareId });
53470
- 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)
53471
54182
  return;
53472
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;
53473
54193
  controller.protocolVersion = heartbeat.protocolVersion;
53474
54194
  controller.runtimeVersion = heartbeat.runtimeVersion;
53475
54195
  controller.capabilities = JSON.stringify(heartbeat.capabilities);
53476
- controller.lastSequence = heartbeat.sequence ?? controller.lastSequence;
53477
- controller.lastHeartbeatAt = now;
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
+ }
53478
54202
  controller.lastSeenAt = now;
53479
54203
  controller.compatibilityError = compatibilityError(heartbeat);
53480
54204
  controller.updatedAt = now;
@@ -53585,7 +54309,8 @@ var WagoService = class {
53585
54309
  }
53586
54310
  connectivity(controller) {
53587
54311
  if (controller.trustState === "untrusted") return "untrusted";
53588
- return controller.lastHeartbeatAt && Date.now() - Date.parse(controller.lastHeartbeatAt) <= STALE_AFTER_MS ? "online" : "stale";
54312
+ const heartbeatAt = this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt;
54313
+ return freshness(heartbeatAt, Date.now(), STALE_AFTER_MS) === "fresh" ? "online" : "stale";
53589
54314
  }
53590
54315
  async appliedRevision(controllerId) {
53591
54316
  const [revision] = await this.revisions.find({
@@ -53757,10 +54482,10 @@ var import_node_fs2 = require("node:fs");
53757
54482
  var import_promises4 = require("node:fs/promises");
53758
54483
  var import_node_os2 = require("node:os");
53759
54484
  var import_node_path2 = require("node:path");
53760
- var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
54485
+ var import_plugins_backend_sdk12 = require("@attraccess/plugins-backend-sdk");
53761
54486
 
53762
54487
  // backend/wago-commissioning-session.entity.ts
53763
- var import_plugins_backend_sdk7 = require("@attraccess/plugins-backend-sdk");
54488
+ var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
53764
54489
  var WagoCommissioningSession = class {
53765
54490
  constructor() {
53766
54491
  __publicField(this, "id");
@@ -53792,85 +54517,85 @@ var WagoCommissioningSession = class {
53792
54517
  }
53793
54518
  };
53794
54519
  __decorateClass([
53795
- (0, import_plugins_backend_sdk7.PrimaryGeneratedColumn)({ type: "integer" })
54520
+ (0, import_plugins_backend_sdk8.PrimaryGeneratedColumn)({ type: "integer" })
53796
54521
  ], WagoCommissioningSession.prototype, "id", 2);
53797
54522
  __decorateClass([
53798
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "hardware_id" })
54523
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "hardware_id" })
53799
54524
  ], WagoCommissioningSession.prototype, "hardwareId", 2);
53800
54525
  __decorateClass([
53801
- (0, import_plugins_backend_sdk7.Column)({ type: "integer", name: "mqtt_server_id" })
54526
+ (0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "mqtt_server_id" })
53802
54527
  ], WagoCommissioningSession.prototype, "mqttServerId", 2);
53803
54528
  __decorateClass([
53804
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "target_host" })
54529
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "target_host" })
53805
54530
  ], WagoCommissioningSession.prototype, "targetHost", 2);
53806
54531
  __decorateClass([
53807
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "host_key_fingerprint" })
54532
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "host_key_fingerprint" })
53808
54533
  ], WagoCommissioningSession.prototype, "hostKeyFingerprint", 2);
53809
54534
  __decorateClass([
53810
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "firmware_baseline" })
54535
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "firmware_baseline" })
53811
54536
  ], WagoCommissioningSession.prototype, "firmwareBaseline", 2);
53812
54537
  __decorateClass([
53813
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "controller_name", nullable: true })
54538
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "controller_name", nullable: true })
53814
54539
  ], WagoCommissioningSession.prototype, "controllerName", 2);
53815
54540
  __decorateClass([
53816
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar" })
54541
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar" })
53817
54542
  ], WagoCommissioningSession.prototype, "state", 2);
53818
54543
  __decorateClass([
53819
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "enrollment_expires_at", nullable: true })
54544
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "enrollment_expires_at", nullable: true })
53820
54545
  ], WagoCommissioningSession.prototype, "enrollmentExpiresAt", 2);
53821
54546
  __decorateClass([
53822
- (0, import_plugins_backend_sdk7.Column)({ type: "integer", name: "enrollment_id", nullable: true })
54547
+ (0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "enrollment_id", nullable: true })
53823
54548
  ], WagoCommissioningSession.prototype, "enrollmentId", 2);
53824
54549
  __decorateClass([
53825
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "pairing_code", nullable: true })
54550
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "pairing_code", nullable: true })
53826
54551
  ], WagoCommissioningSession.prototype, "pairingCode", 2);
53827
54552
  __decorateClass([
53828
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "delivery_token", nullable: true })
54553
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "delivery_token", nullable: true })
53829
54554
  ], WagoCommissioningSession.prototype, "deliveryToken", 2);
53830
54555
  __decorateClass([
53831
- (0, import_plugins_backend_sdk7.Column)({ type: "text", name: "initiating_principal", nullable: true })
54556
+ (0, import_plugins_backend_sdk8.Column)({ type: "text", name: "initiating_principal", nullable: true })
53832
54557
  ], WagoCommissioningSession.prototype, "initiatingPrincipal", 2);
53833
54558
  __decorateClass([
53834
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "runtime_artifact_digest", nullable: true })
54559
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "runtime_artifact_digest", nullable: true })
53835
54560
  ], WagoCommissioningSession.prototype, "runtimeArtifactDigest", 2);
53836
54561
  __decorateClass([
53837
- (0, import_plugins_backend_sdk7.Column)({ type: "integer", name: "management_controller_id", nullable: true })
54562
+ (0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "management_controller_id", nullable: true })
53838
54563
  ], WagoCommissioningSession.prototype, "managementControllerId", 2);
53839
54564
  __decorateClass([
53840
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "docker_provision_token", nullable: true })
54565
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "docker_provision_token", nullable: true })
53841
54566
  ], WagoCommissioningSession.prototype, "dockerProvisionToken", 2);
53842
54567
  __decorateClass([
53843
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "docker_provision_state", nullable: true })
54568
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "docker_provision_state", nullable: true })
53844
54569
  ], WagoCommissioningSession.prototype, "dockerProvisionState", 2);
53845
54570
  __decorateClass([
53846
- (0, import_plugins_backend_sdk7.Column)({ type: "text", name: "platform_report", nullable: true })
54571
+ (0, import_plugins_backend_sdk8.Column)({ type: "text", name: "platform_report", nullable: true })
53847
54572
  ], WagoCommissioningSession.prototype, "platformReport", 2);
53848
54573
  __decorateClass([
53849
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "codesys_state", nullable: true })
54574
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "codesys_state", nullable: true })
53850
54575
  ], WagoCommissioningSession.prototype, "codesysState", 2);
53851
54576
  __decorateClass([
53852
- (0, import_plugins_backend_sdk7.Column)({ type: "integer", name: "progress_percent", nullable: true })
54577
+ (0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "progress_percent", nullable: true })
53853
54578
  ], WagoCommissioningSession.prototype, "progressPercent", 2);
53854
54579
  __decorateClass([
53855
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "progress_step", nullable: true })
54580
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "progress_step", nullable: true })
53856
54581
  ], WagoCommissioningSession.prototype, "progressStep", 2);
53857
54582
  __decorateClass([
53858
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "progress_detail", nullable: true })
54583
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "progress_detail", nullable: true })
53859
54584
  ], WagoCommissioningSession.prototype, "progressDetail", 2);
53860
54585
  __decorateClass([
53861
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "audit_log" })
54586
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "audit_log" })
53862
54587
  ], WagoCommissioningSession.prototype, "auditLog", 2);
53863
54588
  __decorateClass([
53864
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "failure_reason", nullable: true })
54589
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "failure_reason", nullable: true })
53865
54590
  ], WagoCommissioningSession.prototype, "failureReason", 2);
53866
54591
  __decorateClass([
53867
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "created_at" })
54592
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "created_at" })
53868
54593
  ], WagoCommissioningSession.prototype, "createdAt", 2);
53869
54594
  __decorateClass([
53870
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "updated_at" })
54595
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "updated_at" })
53871
54596
  ], WagoCommissioningSession.prototype, "updatedAt", 2);
53872
54597
  WagoCommissioningSession = __decorateClass([
53873
- (0, import_plugins_backend_sdk7.Entity)({ name: "plugin_wago_commissioning_sessions" })
54598
+ (0, import_plugins_backend_sdk8.Entity)({ name: "plugin_wago_commissioning_sessions" })
53874
54599
  ], WagoCommissioningSession);
53875
54600
 
53876
54601
  // backend/wago-commissioning-verification.ts
@@ -54268,13 +54993,13 @@ async function verifyRuntimeSignature(file, armor, trustedKey = WAGO_RUNTIME_REL
54268
54993
  if (!match) invalid();
54269
54994
  const packet = Buffer.from(match[1].replace(/\s/g, ""), "base64");
54270
54995
  if (packet.length < 10 || packet.subarray(0, 6).toString() !== "SSHSIG" || packet.readUInt32BE(6) !== 1) invalid();
54271
- const fields = reader(packet.subarray(10));
54272
- const key = fields.string();
54273
- const namespace = fields.string();
54274
- const reserved = fields.string();
54275
- const algorithm2 = fields.string();
54276
- const signature = reader(fields.string());
54277
- fields.end();
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();
54278
55003
  if (!key.equals(Buffer.from(trustedKey, "base64")) || namespace.toString() !== "attraccess-wago-runtime" || reserved.length)
54279
55004
  invalid();
54280
55005
  if (!["sha256", "sha512"].includes(algorithm2.toString()) || signature.string().toString() !== "ssh-ed25519")
@@ -54739,7 +55464,7 @@ WagoRuntimeArtifactsService = __decorateClass([
54739
55464
 
54740
55465
  // backend/wago-commissioning-readiness.ts
54741
55466
  var import_common5 = require("@nestjs/common");
54742
- var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
55467
+ var import_plugins_backend_sdk9 = require("@attraccess/plugins-backend-sdk");
54743
55468
  var WagoCommissioningReadiness = class {
54744
55469
  constructor(context) {
54745
55470
  __publicField(this, "context", context);
@@ -54803,14 +55528,14 @@ var WagoCommissioningReadiness = class {
54803
55528
  };
54804
55529
  WagoCommissioningReadiness = __decorateClass([
54805
55530
  (0, import_common5.Injectable)(),
54806
- __decorateParam(0, (0, import_common5.Inject)(import_plugins_backend_sdk8.PLUGIN_CONTEXT))
55531
+ __decorateParam(0, (0, import_common5.Inject)(import_plugins_backend_sdk9.PLUGIN_CONTEXT))
54807
55532
  ], WagoCommissioningReadiness);
54808
55533
 
54809
55534
  // backend/wago-commissioning-lease.ts
54810
55535
  var import_node_crypto8 = require("node:crypto");
54811
55536
 
54812
55537
  // backend/wago-commissioning-lease.entity.ts
54813
- var import_plugins_backend_sdk9 = require("@attraccess/plugins-backend-sdk");
55538
+ var import_plugins_backend_sdk10 = require("@attraccess/plugins-backend-sdk");
54814
55539
  var WagoCommissioningLeaseEntity = class {
54815
55540
  constructor() {
54816
55541
  __publicField(this, "fingerprintHash");
@@ -54821,22 +55546,22 @@ var WagoCommissioningLeaseEntity = class {
54821
55546
  }
54822
55547
  };
54823
55548
  __decorateClass([
54824
- (0, import_plugins_backend_sdk9.PrimaryColumn)({ type: "varchar", name: "fingerprint_hash", length: 64 })
55549
+ (0, import_plugins_backend_sdk10.PrimaryColumn)({ type: "varchar", name: "fingerprint_hash", length: 64 })
54825
55550
  ], WagoCommissioningLeaseEntity.prototype, "fingerprintHash", 2);
54826
55551
  __decorateClass([
54827
- (0, import_plugins_backend_sdk9.Column)({ type: "varchar", name: "owner" })
55552
+ (0, import_plugins_backend_sdk10.Column)({ type: "varchar", name: "owner" })
54828
55553
  ], WagoCommissioningLeaseEntity.prototype, "owner", 2);
54829
55554
  __decorateClass([
54830
- (0, import_plugins_backend_sdk9.Column)({ type: "bigint", name: "lease_until" })
55555
+ (0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "lease_until" })
54831
55556
  ], WagoCommissioningLeaseEntity.prototype, "leaseUntil", 2);
54832
55557
  __decorateClass([
54833
- (0, import_plugins_backend_sdk9.Column)({ type: "bigint", name: "operation_until" })
55558
+ (0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "operation_until" })
54834
55559
  ], WagoCommissioningLeaseEntity.prototype, "operationUntil", 2);
54835
55560
  __decorateClass([
54836
- (0, import_plugins_backend_sdk9.Column)({ type: "bigint", name: "recovery_after" })
55561
+ (0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "recovery_after" })
54837
55562
  ], WagoCommissioningLeaseEntity.prototype, "recoveryAfter", 2);
54838
55563
  WagoCommissioningLeaseEntity = __decorateClass([
54839
- (0, import_plugins_backend_sdk9.Entity)({ name: "plugin_wago_commissioning_lease" })
55564
+ (0, import_plugins_backend_sdk10.Entity)({ name: "plugin_wago_commissioning_lease" })
54840
55565
  ], WagoCommissioningLeaseEntity);
54841
55566
 
54842
55567
  // backend/wago-commissioning-lease.ts
@@ -55012,7 +55737,7 @@ function createWagoCommissioningLeaseService(context) {
55012
55737
  }
55013
55738
 
55014
55739
  // backend/wago-management.entity.ts
55015
- var import_plugins_backend_sdk10 = require("@attraccess/plugins-backend-sdk");
55740
+ var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
55016
55741
  var WagoManagementEntity = class {
55017
55742
  constructor() {
55018
55743
  __publicField(this, "controllerId");
@@ -55023,22 +55748,22 @@ var WagoManagementEntity = class {
55023
55748
  }
55024
55749
  };
55025
55750
  __decorateClass([
55026
- (0, import_plugins_backend_sdk10.PrimaryColumn)({ type: "integer", name: "controller_id" })
55751
+ (0, import_plugins_backend_sdk11.PrimaryColumn)({ type: "integer", name: "controller_id" })
55027
55752
  ], WagoManagementEntity.prototype, "controllerId", 2);
55028
55753
  __decorateClass([
55029
- (0, import_plugins_backend_sdk10.Column)({ type: "text", name: "metadata_json", nullable: true })
55754
+ (0, import_plugins_backend_sdk11.Column)({ type: "text", name: "metadata_json", nullable: true })
55030
55755
  ], WagoManagementEntity.prototype, "metadataJson", 2);
55031
55756
  __decorateClass([
55032
- (0, import_plugins_backend_sdk10.Column)({ type: "text", name: "encrypted_private_key", nullable: true, select: false })
55757
+ (0, import_plugins_backend_sdk11.Column)({ type: "text", name: "encrypted_private_key", nullable: true, select: false })
55033
55758
  ], WagoManagementEntity.prototype, "encryptedPrivateKey", 2);
55034
55759
  __decorateClass([
55035
- (0, import_plugins_backend_sdk10.Column)({ type: "varchar", name: "lease_owner", nullable: true })
55760
+ (0, import_plugins_backend_sdk11.Column)({ type: "varchar", name: "lease_owner", nullable: true })
55036
55761
  ], WagoManagementEntity.prototype, "leaseOwner", 2);
55037
55762
  __decorateClass([
55038
- (0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "lease_until", default: 0 })
55763
+ (0, import_plugins_backend_sdk11.Column)({ type: "bigint", name: "lease_until", default: 0 })
55039
55764
  ], WagoManagementEntity.prototype, "leaseUntil", 2);
55040
55765
  WagoManagementEntity = __decorateClass([
55041
- (0, import_plugins_backend_sdk10.Entity)({ name: "plugin_wago_management" })
55766
+ (0, import_plugins_backend_sdk11.Entity)({ name: "plugin_wago_management" })
55042
55767
  ], WagoManagementEntity);
55043
55768
 
55044
55769
  // backend/wago-management-inspection.ts
@@ -55503,7 +56228,7 @@ var transitionStates = [
55503
56228
  "recovery_required"
55504
56229
  ];
55505
56230
  var exceptionNames = ["wbm_exposed", "other_services_exposed", "unqualified_privileges"];
55506
- var identifier = () => (0, import_node_crypto10.randomBytes)(16).toString("hex");
56231
+ var identifier2 = () => (0, import_node_crypto10.randomBytes)(16).toString("hex");
55507
56232
  var REVIEW_MS = 3e5;
55508
56233
  var LEASE_MS = 30 * 6e4;
55509
56234
  var ManagementError = class extends Error {
@@ -55570,7 +56295,7 @@ var WagoManagementService = class {
55570
56295
  record3.support = this.adapter.qualify(record3.inspection, input.mode).support;
55571
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")))
55572
56297
  throw new ManagementError("invalid_request");
55573
- record3.reviewToken = identifier();
56298
+ record3.reviewToken = identifier2();
55574
56299
  record3.reviewedAt = this.now();
55575
56300
  record3.state = "reviewed";
55576
56301
  await this.save(record3, owner, guard);
@@ -55603,7 +56328,7 @@ var WagoManagementService = class {
55603
56328
  if (this.secrets.decrypt(record2.encryptedPrivateKey) !== key.privateKey) throw new Error();
55604
56329
  record2.keyFingerprint = key.fingerprint;
55605
56330
  record2.transaction = {
55606
- id: identifier(),
56331
+ id: identifier2(),
55607
56332
  target: record2.target,
55608
56333
  username: input.temporarySsh.username,
55609
56334
  deadline: this.now() + 15e4
@@ -55659,7 +56384,7 @@ var WagoManagementService = class {
55659
56384
  });
55660
56385
  }
55661
56386
  async verify(record2, privateKey, guard) {
55662
- const nonce = identifier();
56387
+ const nonce = identifier2();
55663
56388
  const tx = this.transaction(record2);
55664
56389
  const proof = await this.remote(guard, () => this.adapter.verifyKey(tx, privateKey, nonce));
55665
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)
@@ -55731,7 +56456,7 @@ var WagoManagementService = class {
55731
56456
  }
55732
56457
  async locked(controllerId, fingerprint2, action) {
55733
56458
  validId(controllerId);
55734
- const owner = identifier();
56459
+ const owner = identifier2();
55735
56460
  let acquired = false;
55736
56461
  let mutationStarted = false;
55737
56462
  try {
@@ -57342,7 +58067,7 @@ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
57342
58067
  };
57343
58068
  WagoCommissioningService = __decorateClass([
57344
58069
  (0, import_common6.Injectable)(),
57345
- __decorateParam(0, (0, import_common6.Inject)(import_plugins_backend_sdk11.PLUGIN_CONTEXT)),
58070
+ __decorateParam(0, (0, import_common6.Inject)(import_plugins_backend_sdk12.PLUGIN_CONTEXT)),
57346
58071
  __decorateParam(1, (0, import_common6.Inject)(WagoService)),
57347
58072
  __decorateParam(2, (0, import_common6.Optional)()),
57348
58073
  __decorateParam(2, (0, import_common6.Inject)(WagoRuntimeArtifactsService)),
@@ -57777,17 +58502,23 @@ var WagoControllerApi = class {
57777
58502
  }
57778
58503
  previewPreset(id, body) {
57779
58504
  if (!body?.application) throw new import_common8.BadRequestException("application is required");
57780
- return this.wago.previewPreset(id, body.application);
58505
+ return this.wago.previewPreset(id, body.application, body.snapshot);
57781
58506
  }
57782
58507
  applyPreset(id, body) {
57783
58508
  if (!body?.application) throw new import_common8.BadRequestException("application is required");
57784
- return this.wago.applyPreset(id, body.application, body.selectedPaths ?? [], body.previewedDraftHash ?? "");
58509
+ return this.wago.applyPreset(
58510
+ id,
58511
+ body.application,
58512
+ body.selectedPaths ?? [],
58513
+ body.previewedDraftHash ?? "",
58514
+ body.snapshot
58515
+ );
57785
58516
  }
57786
58517
  saveDraft(id, body) {
57787
- return this.wago.saveDraft(id, body?.snapshot);
58518
+ return this.wago.saveDraft(id, body?.snapshot, body?.metadata);
57788
58519
  }
57789
- validateDraft(id) {
57790
- return this.wago.validateDraft(id);
58520
+ validateDraft(id, body) {
58521
+ return this.wago.validateDraft(id, body?.snapshot);
57791
58522
  }
57792
58523
  reviewDraft(id) {
57793
58524
  return this.wago.reviewDraft(id);
@@ -57795,23 +58526,31 @@ var WagoControllerApi = class {
57795
58526
  revisions(id, offset, limit) {
57796
58527
  return this.wago.revisionsFor(id, Number(offset), Number(limit));
57797
58528
  }
57798
- publishDraft(id, request) {
58529
+ publishDraft(id, request, body) {
57799
58530
  return this.audit.run(
57800
58531
  wagoAuditPrincipal(request),
57801
58532
  id,
57802
58533
  "publication",
57803
- {},
57804
- () => this.wago.publishDraft(id),
58534
+ body?.force === true ? { force: true } : {},
58535
+ () => this.wago.publishDraft(id, body?.force === true, body?.reviewedHash),
57805
58536
  (published) => ({ revision: published.revision })
57806
58537
  );
57807
58538
  }
57808
- rollback(id, revision, request) {
58539
+ rollback(id, revision, request, body) {
57809
58540
  return this.audit.run(
57810
58541
  wagoAuditPrincipal(request),
57811
58542
  id,
57812
58543
  "rollback",
57813
- { sourceRevision: revision },
57814
- () => this.wago.rollback(id, revision),
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
+ ),
57815
58554
  (published) => ({ revision: published.revision })
57816
58555
  );
57817
58556
  }
@@ -57823,61 +58562,61 @@ __decorateClass([
57823
58562
  (0, import_common8.Get)("controllers")
57824
58563
  ], WagoControllerApi.prototype, "list", 1);
57825
58564
  __decorateClass([
57826
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58565
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57827
58566
  (0, import_common8.Get)("settings")
57828
58567
  ], WagoControllerApi.prototype, "settings", 1);
57829
58568
  __decorateClass([
57830
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58569
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57831
58570
  (0, import_common8.Post)("settings"),
57832
58571
  __decorateParam(0, (0, import_common8.Body)())
57833
58572
  ], WagoControllerApi.prototype, "setSettings", 1);
57834
58573
  __decorateClass([
57835
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58574
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57836
58575
  (0, import_common8.Get)("commissioning/support")
57837
58576
  ], WagoControllerApi.prototype, "commissioningSupport", 1);
57838
58577
  __decorateClass([
57839
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58578
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57840
58579
  (0, import_common8.Get)("commissioning/sessions"),
57841
58580
  __decorateParam(0, (0, import_common8.Query)("limit")),
57842
58581
  __decorateParam(1, (0, import_common8.Query)("offset"))
57843
58582
  ], WagoControllerApi.prototype, "commissioningSessions", 1);
57844
58583
  __decorateClass([
57845
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58584
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57846
58585
  (0, import_common8.Post)("commissioning/sessions"),
57847
58586
  __decorateParam(0, (0, import_common8.Body)())
57848
58587
  ], WagoControllerApi.prototype, "createCommissioningSession", 1);
57849
58588
  __decorateClass([
57850
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58589
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57851
58590
  (0, import_common8.Post)("commissioning/sessions/:id/confirm-host-key"),
57852
58591
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57853
58592
  __decorateParam(1, (0, import_common8.Body)())
57854
58593
  ], WagoControllerApi.prototype, "confirmCommissioningHostKey", 1);
57855
58594
  __decorateClass([
57856
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58595
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57857
58596
  (0, import_common8.Post)("commissioning/sessions/:id/deliver"),
57858
58597
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57859
58598
  __decorateParam(1, (0, import_common8.Body)()),
57860
58599
  __decorateParam(2, (0, import_common8.Req)())
57861
58600
  ], WagoControllerApi.prototype, "deliverCommissioningSession", 1);
57862
58601
  __decorateClass([
57863
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58602
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57864
58603
  (0, import_common8.Post)("commissioning/sessions/:id/recover"),
57865
58604
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57866
58605
  __decorateParam(1, (0, import_common8.Body)()),
57867
58606
  __decorateParam(2, (0, import_common8.Req)())
57868
58607
  ], WagoControllerApi.prototype, "recoverCommissioningSession", 1);
57869
58608
  __decorateClass([
57870
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58609
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57871
58610
  (0, import_common8.Get)("commissioning/sessions/:id/verification"),
57872
58611
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
57873
58612
  ], WagoControllerApi.prototype, "commissioningVerification", 1);
57874
58613
  __decorateClass([
57875
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58614
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57876
58615
  (0, import_common8.Post)("commissioning/sessions/:id/revoke"),
57877
58616
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
57878
58617
  ], WagoControllerApi.prototype, "revokeCommissioningSession", 1);
57879
58618
  __decorateClass([
57880
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58619
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57881
58620
  (0, import_common8.Delete)("commissioning/sessions/:id"),
57882
58621
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
57883
58622
  ], WagoControllerApi.prototype, "removeCommissioningSession", 1);
@@ -57916,7 +58655,8 @@ __decorateClass([
57916
58655
  ], WagoControllerApi.prototype, "saveDraft", 1);
57917
58656
  __decorateClass([
57918
58657
  (0, import_common8.Post)("controllers/:id/configuration/validate"),
57919
- __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)())
57920
58660
  ], WagoControllerApi.prototype, "validateDraft", 1);
57921
58661
  __decorateClass([
57922
58662
  (0, import_common8.Post)("controllers/:id/configuration/review"),
@@ -57931,13 +58671,15 @@ __decorateClass([
57931
58671
  __decorateClass([
57932
58672
  (0, import_common8.Post)("controllers/:id/configuration/publish"),
57933
58673
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57934
- __decorateParam(1, (0, import_common8.Req)())
58674
+ __decorateParam(1, (0, import_common8.Req)()),
58675
+ __decorateParam(2, (0, import_common8.Body)())
57935
58676
  ], WagoControllerApi.prototype, "publishDraft", 1);
57936
58677
  __decorateClass([
57937
58678
  (0, import_common8.Post)("controllers/:id/configuration/rollback/:revision"),
57938
58679
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57939
58680
  __decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe)),
57940
- __decorateParam(2, (0, import_common8.Req)())
58681
+ __decorateParam(2, (0, import_common8.Req)()),
58682
+ __decorateParam(3, (0, import_common8.Body)())
57941
58683
  ], WagoControllerApi.prototype, "rollback", 1);
57942
58684
  __decorateClass([
57943
58685
  (0, import_common8.Get)("controllers/:id/configuration/revisions/:revision/preview"),
@@ -57945,7 +58687,7 @@ __decorateClass([
57945
58687
  __decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe))
57946
58688
  ], WagoControllerApi.prototype, "previewRevision", 1);
57947
58689
  WagoControllerApi = __decorateClass([
57948
- (0, import_plugins_backend_sdk12.Auth)("resources.update"),
58690
+ (0, import_plugins_backend_sdk13.Auth)("resources.update"),
57949
58691
  (0, import_common8.Controller)("wago"),
57950
58692
  __decorateParam(0, (0, import_common8.Inject)(WagoService)),
57951
58693
  __decorateParam(1, (0, import_common8.Inject)(WagoCommissioningService)),
@@ -57954,7 +58696,7 @@ WagoControllerApi = __decorateClass([
57954
58696
 
57955
58697
  // backend/wago-artifacts.controller.ts
57956
58698
  var import_common9 = require("@nestjs/common");
57957
- var import_plugins_backend_sdk13 = require("@attraccess/plugins-backend-sdk");
58699
+ var import_plugins_backend_sdk14 = require("@attraccess/plugins-backend-sdk");
57958
58700
  var import_platform_express = __toESM(require_platform_express());
57959
58701
  var import_promises5 = require("node:fs/promises");
57960
58702
  var import_node_path3 = require("node:path");
@@ -58172,7 +58914,7 @@ __decorateClass([
58172
58914
  __decorateParam(0, (0, import_common9.UploadedFiles)())
58173
58915
  ], WagoArtifactsController.prototype, "import", 1);
58174
58916
  WagoArtifactsController = __decorateClass([
58175
- (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
58917
+ (0, import_plugins_backend_sdk14.Auth)("system.settings.manage"),
58176
58918
  (0, import_common9.Controller)("wago/runtime-artifacts"),
58177
58919
  __decorateParam(0, (0, import_common9.Inject)(WagoRuntimeArtifactsService))
58178
58920
  ], WagoArtifactsController);
@@ -58642,6 +59384,167 @@ WagoFlowService = __decorateClass([
58642
59384
  __decorateParam(0, (0, import_common10.Inject)(PLUGIN_CONTEXT4))
58643
59385
  ], WagoFlowService);
58644
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
+
58645
59548
  // backend/plugin.ts
58646
59549
  var PLUGIN_CONTEXT5 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
58647
59550
  var WagoPluginModule = class {
@@ -58698,7 +59601,7 @@ var plugin = {
58698
59601
  flowService = new WagoFlowService(context);
58699
59602
  return {
58700
59603
  module: WagoPluginModule,
58701
- controllers: [WagoControllerApi, WagoArtifactsController],
59604
+ controllers: [WagoControllerApi, WagoArtifactsController, WagoDiagnosticsController],
58702
59605
  providers: [
58703
59606
  { provide: PLUGIN_CONTEXT5, useValue: context },
58704
59607
  WagoService,
@@ -58706,6 +59609,7 @@ var plugin = {
58706
59609
  WagoArtifactUploadInterceptor,
58707
59610
  WagoCommissioningReadiness,
58708
59611
  WagoCommissioningService,
59612
+ WagoDiagnosticsService,
58709
59613
  { provide: WagoFlowService, useValue: flowService }
58710
59614
  ]
58711
59615
  };