@attraccess/plugin-wago 0.1.0-nightly.952.1 → 0.1.0-nightly.954.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -661,9 +661,9 @@ var require_vary = __commonJS({
661
661
  if (!field2) {
662
662
  throw new TypeError("field argument is required");
663
663
  }
664
- var 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 {
@@ -52451,7 +52658,19 @@ var WagoCommandHandler = class {
52451
52658
  const revision = controllerId ? await this.dependencies.appliedRevision(controllerId) : null;
52452
52659
  const snapshot = revision ? JSON.parse(revision.snapshot) : null;
52453
52660
  const channelId = typeof config.channelId === "string" ? config.channelId : void 0;
52454
- const channel = snapshot?.logicalChannels.find((item) => item.id === channelId);
52661
+ const outputChannels = snapshot?.logicalChannels.filter((item) => item.capabilities.includes("output")) ?? [];
52662
+ let names = {};
52663
+ if (controllerId) {
52664
+ const draft = await this.dependencies.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId });
52665
+ try {
52666
+ const draftMatchesAppliedRevision = typeof draft?.snapshot === "string" && configurationHash(JSON.parse(draft.snapshot)) === revision?.contentHash;
52667
+ const provenance = revision?.presetProvenance ?? (draftMatchesAppliedRevision ? draft?.presetProvenance : null);
52668
+ const storedNames = JSON.parse(provenance ?? "null")?.editor?.names;
52669
+ if (storedNames && typeof storedNames === "object" && !Array.isArray(storedNames)) names = storedNames;
52670
+ } catch {
52671
+ }
52672
+ }
52673
+ const channel = outputChannels.find((item) => item.id === channelId);
52455
52674
  const references = channelId && controllerId ? await this.references(controllerId, channelId, resourceId) : [];
52456
52675
  const properties = {
52457
52676
  controllerId: {
@@ -52462,16 +52681,20 @@ var WagoCommandHandler = class {
52462
52681
  const: controller.id,
52463
52682
  title: controller.name ?? controller.hardwareId
52464
52683
  })),
52465
- refreshesSchema: true
52684
+ refreshesSchema: true,
52685
+ description: controllerId && !revision ? "Publish a configuration and wait for the controller to apply it before authoring commands." : void 0
52466
52686
  }
52467
52687
  };
52468
52688
  if (controllerId && revision && snapshot) {
52469
52689
  properties.channelId = {
52470
52690
  type: "string",
52471
52691
  title: "Logical Channel",
52472
- oneOf: snapshot.logicalChannels.map((item) => ({ const: item.id, title: `${item.id} (${item.profile})` })),
52692
+ oneOf: outputChannels.map((item) => ({
52693
+ const: item.id,
52694
+ title: typeof names[item.id] === "string" ? names[item.id] : `${item.id} (${item.profile})`
52695
+ })),
52473
52696
  refreshesSchema: true,
52474
- helpText: references.length ? `Also controlled by resource flow node${references.length === 1 ? "" : "s"}: ${references.join(", ")}. Reuse is allowed.` : void 0
52697
+ description: references.length ? `Also controlled by resource flow node${references.length === 1 ? "" : "s"}: ${references.join(", ")}. Reuse is allowed.` : outputChannels.length ? void 0 : "This applied configuration has no output channels. Add an output and publish it first."
52475
52698
  };
52476
52699
  }
52477
52700
  if (channel) {
@@ -52480,7 +52703,7 @@ var WagoCommandHandler = class {
52480
52703
  ...channel.capabilities.includes("pulse") ? [{ const: "pulse", title: "Pulse" }] : []
52481
52704
  ] : [];
52482
52705
  properties.action = { type: "string", title: "Operation", oneOf: actions, refreshesSchema: true };
52483
- if (config.action === "set") properties.value = { type: "boolean", title: "State" };
52706
+ if (config.action === "set") properties.value = { type: "boolean", title: "State", default: false };
52484
52707
  properties.expectedConfigurationRevision = {
52485
52708
  type: "number",
52486
52709
  title: "Configuration revision",
@@ -52521,7 +52744,15 @@ var WagoCommandHandler = class {
52521
52744
  dynamic: true,
52522
52745
  type: "object",
52523
52746
  properties,
52524
- required: Object.keys(properties).filter((key) => key !== "value")
52747
+ required: [
52748
+ .../* @__PURE__ */ new Set([
52749
+ "controllerId",
52750
+ "channelId",
52751
+ "action",
52752
+ "expectedConfigurationRevision",
52753
+ ...Object.keys(properties)
52754
+ ])
52755
+ ]
52525
52756
  };
52526
52757
  }
52527
52758
  async validate(config, validationContext = /* @__PURE__ */ new Map()) {
@@ -52578,6 +52809,7 @@ var WagoCommandHandler = class {
52578
52809
  throw new WagoCommandError(`WAGO controller ${controllerId} has no MQTT server`, "transport-dispatch");
52579
52810
  const settings = await this.dependencies.getSettings();
52580
52811
  const id = (0, import_node_crypto2.randomUUID)();
52812
+ this.dependencies.onCommand?.(controllerId, channelId, id);
52581
52813
  const command = JSON.stringify({
52582
52814
  id,
52583
52815
  expiresAt: new Date(Date.now() + acknowledgementTimeoutSeconds * 1e3).toISOString(),
@@ -52595,6 +52827,7 @@ var WagoCommandHandler = class {
52595
52827
  { qos: 1, retain: false }
52596
52828
  );
52597
52829
  } catch (error) {
52830
+ this.dependencies.onCommandFailure?.(id, "dispatch-failed");
52598
52831
  const dispatchError = new WagoCommandError(
52599
52832
  `Failed to publish WAGO command: ${String(error)}`,
52600
52833
  "transport-dispatch"
@@ -52634,7 +52867,7 @@ var WagoCommandHandler = class {
52634
52867
  }
52635
52868
  }
52636
52869
  async references(controllerId, channelId, resourceId) {
52637
- const nodes = await this.dependencies.context.dataSource.getRepository(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();
52638
52871
  return nodes.map((node) => `resource ${node.resourceId} / node ${node.id}`);
52639
52872
  }
52640
52873
  parse(config) {
@@ -52716,6 +52949,8 @@ var WagoCommandHandler = class {
52716
52949
  pending.resolve();
52717
52950
  }
52718
52951
  reject(id, error) {
52952
+ if (error instanceof WagoCommandError && error.kind === "acknowledgement-timeout")
52953
+ this.dependencies.onCommandFailure?.(id, "timeout");
52719
52954
  const pending = this.pending.get(id);
52720
52955
  if (!pending) return;
52721
52956
  this.pending.delete(id);
@@ -52734,6 +52969,349 @@ function positiveInteger(value) {
52734
52969
  return Number.isSafeInteger(value) && value > 0 ? value : void 0;
52735
52970
  }
52736
52971
 
52972
+ // backend/diagnostics-envelope.ts
52973
+ var DIAGNOSTIC_CATEGORIES = [
52974
+ "heartbeat",
52975
+ "state",
52976
+ "measurements",
52977
+ "faults",
52978
+ "acknowledgements",
52979
+ "configuration/reported"
52980
+ ];
52981
+ var MILLI_UNITS = ["milliampere", "millivolt", "milliwatt", "milliwatt-hour", "millipercent"];
52982
+ var CANONICAL_UNITS = [...MILLI_UNITS, "ampere", "volt", "watt", "watt-hour", "percent"];
52983
+ function sourceTime(value) {
52984
+ if (typeof value !== "string") return null;
52985
+ const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,3})?(Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
52986
+ if (!match || Number(match[4] ?? 0) > 23 || Number(match[5] ?? 0) > 59) return null;
52987
+ const time = Date.parse(value);
52988
+ if (!Number.isFinite(time)) return null;
52989
+ const offset = match[2] === "Z" ? 0 : (match[3] === "+" ? 1 : -1) * (Number(match[4]) * 60 + Number(match[5])) * 6e4;
52990
+ return new Date(time + offset).toISOString().slice(0, 19) === match[1] ? time : null;
52991
+ }
52992
+ function emptyStream() {
52993
+ return { lastSourceTime: 0, retiredStreams: [], watermarks: {}, sequenceGaps: 0, trackingExhausted: false };
52994
+ }
52995
+ function canonicalEnvelope(data, category) {
52996
+ return "timestamp" in data || "streamId" in data || category !== "heartbeat" && "sequence" in data;
52997
+ }
52998
+ function validEnvelope(data, now) {
52999
+ const time = sourceTime(data.timestamp);
53000
+ return time !== null && time <= now && typeof data.streamId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.streamId) && Number.isSafeInteger(data.sequence) && data.sequence >= 1;
53001
+ }
53002
+ function admitEnvelope(state, data, category, now) {
53003
+ const stream = data.streamId.toLowerCase();
53004
+ const timestamp = sourceTime(data.timestamp);
53005
+ const sequence = data.sequence;
53006
+ if (state.retiredStreams.includes(stream) || state.trackingExhausted) return "rejected";
53007
+ const restart = state.activeStream !== stream;
53008
+ if (restart) {
53009
+ if (!(category === "heartbeat" || category === "state" && data.connected === true) || now - timestamp > 9e4 || timestamp < state.lastSourceTime)
53010
+ return "rejected";
53011
+ if (state.retiredStreams.length >= 16) {
53012
+ state.trackingExhausted = true;
53013
+ return "rejected";
53014
+ }
53015
+ }
53016
+ const previous = restart ? void 0 : state.watermarks[category];
53017
+ if (previous && (sequence <= previous.sequence || timestamp < previous.timestamp)) return "rejected";
53018
+ if (restart) {
53019
+ if (state.activeStream) state.retiredStreams.push(state.activeStream);
53020
+ state.activeStream = stream;
53021
+ state.watermarks = {};
53022
+ }
53023
+ state.sequenceGaps = Math.min(
53024
+ Number.MAX_SAFE_INTEGER,
53025
+ state.sequenceGaps + Math.max(0, sequence - (previous?.sequence ?? 0) - 1)
53026
+ );
53027
+ state.watermarks[category] = { sequence, timestamp };
53028
+ state.lastSourceTime = Math.max(state.lastSourceTime, timestamp);
53029
+ return restart ? "restart" : "accepted";
53030
+ }
53031
+
53032
+ // backend/diagnostics-validation.ts
53033
+ var fields = new Set(
53034
+ "version physicalPoints logicalChannels id physicalPointId hardwareProfile channel profile capabilities disconnectPolicy mode timeoutMs range minimum maximum pulse durationMs guard channelId when feedback expected measurement unit scale offset".split(
53035
+ " "
53036
+ )
53037
+ );
53038
+ var codes = new Set(
53039
+ "invalid_snapshot unsupported_version invalid_channel invalid_collection invalid_object unknown_field required_field invalid_id duplicate_id unsupported_value invalid_capabilities unsupported_capability duplicate_capability unsupported_field invalid_range invalid_duration invalid_measurement invalid_feedback_channel invalid_timeout missing_capability unknown_reference invalid_reference content_hash_mismatch revision_mismatch hardware_unavailable".split(
53040
+ " "
53041
+ )
53042
+ );
53043
+ function safeValidationSummaries(value) {
53044
+ if (!Array.isArray(value)) return [];
53045
+ return value.slice(0, 50).map((item) => {
53046
+ const error = item && typeof item === "object" ? item : {};
53047
+ const path = typeof error.path === "string" && error.path.length <= 256 ? error.path : "$";
53048
+ const tokens = path.replace(/\[(\d{1,5})\]/g, ".$1").split(".");
53049
+ const safe = tokens[0] === "$" && tokens.slice(1).every((token) => fields.has(token) || /^\d{1,5}$/.test(token));
53050
+ return {
53051
+ path: safe ? path : "$",
53052
+ code: codes.has(error.code) ? error.code : "validation_error"
53053
+ };
53054
+ });
53055
+ }
53056
+
53057
+ // backend/diagnostics-store.ts
53058
+ function freshness(timestamp, now = Date.now(), maxAge = 9e4) {
53059
+ if (!timestamp) return "missing";
53060
+ const time = sourceTime(timestamp);
53061
+ if (time === null) return "invalid";
53062
+ if (time > now) return "future";
53063
+ return now - time > maxAge ? "stale" : "fresh";
53064
+ }
53065
+ var MAX_CONTROLLERS = 256;
53066
+ var MAX_CHANNELS = 256;
53067
+ var RETENTION_MS = 15 * 6e4;
53068
+ var faultCodes = /* @__PURE__ */ new Set([
53069
+ "measurement_read_failed",
53070
+ "device_write_failed",
53071
+ "feedback_mismatch",
53072
+ "feedback_read_failed"
53073
+ ]);
53074
+ function identifier(value) {
53075
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 256 && !Array.from(value).some((character) => character.charCodeAt(0) < 32);
53076
+ }
53077
+ var WagoDiagnosticsStore = class {
53078
+ constructor(now = Date.now) {
53079
+ __publicField(this, "now", now);
53080
+ __publicField(this, "controllers", /* @__PURE__ */ new Map());
53081
+ __publicField(this, "commands", /* @__PURE__ */ new Map());
53082
+ }
53083
+ prune() {
53084
+ const cutoff = this.now() - RETENTION_MS;
53085
+ for (const [id, state] of this.controllers) if (state.touched < cutoff) this.controllers.delete(id);
53086
+ for (const [id, command] of this.commands) if (command.at < cutoff) this.commands.delete(id);
53087
+ }
53088
+ read(id) {
53089
+ this.prune();
53090
+ const state = this.controllers.get(id);
53091
+ const copy = state ? JSON.parse(JSON.stringify(state)) : {
53092
+ touched: 0,
53093
+ inputs: {},
53094
+ outputs: {},
53095
+ measurements: {},
53096
+ cumulativeMeasurements: {},
53097
+ faults: {},
53098
+ acknowledgements: {},
53099
+ events: [],
53100
+ ...emptyStream()
53101
+ };
53102
+ const cutoff = this.now() - RETENTION_MS;
53103
+ for (const field2 of [
53104
+ "inputs",
53105
+ "outputs",
53106
+ "measurements",
53107
+ "cumulativeMeasurements",
53108
+ "faults",
53109
+ "acknowledgements"
53110
+ ]) {
53111
+ copy[field2] = Object.assign(/* @__PURE__ */ Object.create(null), copy[field2]);
53112
+ }
53113
+ for (const collection2 of [
53114
+ copy.cumulativeMeasurements,
53115
+ copy.inputs,
53116
+ copy.outputs,
53117
+ copy.measurements,
53118
+ copy.faults,
53119
+ copy.acknowledgements
53120
+ ]) {
53121
+ for (const key of Object.keys(collection2))
53122
+ if (Date.parse(collection2[key].receivedAt) < cutoff) delete collection2[key];
53123
+ }
53124
+ copy.events = copy.events.filter((event) => Date.parse(event.receivedAt) >= cutoff);
53125
+ if (copy.rejection && Date.parse(copy.rejection.receivedAt) < cutoff) delete copy.rejection;
53126
+ return copy;
53127
+ }
53128
+ command(controllerId, channelId, id) {
53129
+ this.prune();
53130
+ if (!identifier(channelId) || !identifier(id)) return;
53131
+ if (!this.controllers.has(controllerId) && this.controllers.size >= MAX_CONTROLLERS) return;
53132
+ const state = this.read(controllerId);
53133
+ state.touched = this.now();
53134
+ this.controllers.set(controllerId, state);
53135
+ const oldest = this.commands.keys().next().value;
53136
+ if (this.commands.size >= 1024 && oldest !== void 0) this.commands.delete(oldest);
53137
+ this.commands.set(id, { controllerId, channelId, at: this.now() });
53138
+ }
53139
+ commandFailed(id, status) {
53140
+ this.prune();
53141
+ const command = this.commands.get(id);
53142
+ if (!command) return;
53143
+ this.commands.delete(id);
53144
+ const state = this.controllers.get(command.controllerId);
53145
+ if (!state) return;
53146
+ state.touched = this.now();
53147
+ state.acknowledgements[command.channelId] = { id, status, receivedAt: new Date(this.now()).toISOString() };
53148
+ while (Object.keys(state.acknowledgements).length > MAX_CHANNELS)
53149
+ delete state.acknowledgements[Object.keys(state.acknowledgements)[0]];
53150
+ }
53151
+ canTrack(id) {
53152
+ this.prune();
53153
+ return this.controllers.has(id) || this.controllers.size < MAX_CONTROLLERS || [...this.controllers.values()].some((state) => !state.activeStream);
53154
+ }
53155
+ ingest(id, kind, payload) {
53156
+ this.prune();
53157
+ if (payload.length > 65536) return false;
53158
+ let data;
53159
+ try {
53160
+ data = JSON.parse(payload.toString("utf8"));
53161
+ } catch {
53162
+ return false;
53163
+ }
53164
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
53165
+ if (!DIAGNOSTIC_CATEGORIES.includes(kind)) return false;
53166
+ const canonical = canonicalEnvelope(data, kind);
53167
+ if (canonical && !validEnvelope(data, this.now())) return false;
53168
+ const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
53169
+ if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
53170
+ return false;
53171
+ if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
53172
+ return false;
53173
+ if (canonical && kind === "configuration/reported" && (!Number.isSafeInteger(data.revision) || data.revision < 1 || typeof data.contentHash !== "string" || !/^[0-9a-f]{64}$/i.test(data.contentHash) || !Array.isArray(data.errors)))
53174
+ return false;
53175
+ if (canonical && kind === "state" && (typeof data.connected !== "boolean" || !(data.revision === null || Number.isSafeInteger(data.revision) && data.revision >= 0) || !isObject(data.outputs) || data.inputs !== void 0 && !isObject(data.inputs) || ![...Object.values(data.outputs), ...Object.values(isObject(data.inputs) ? data.inputs : {})].every(
53176
+ (value) => typeof value === "boolean"
53177
+ )))
53178
+ return false;
53179
+ if (kind === "measurements" && (!identifier(data.channelId) || typeof data.value !== "number" || !Number.isFinite(data.value) || (canonical ? !Number.isSafeInteger(data.value) || !CANONICAL_UNITS.includes(data.unit) || !["live", "cumulative"].includes(data.kind) : !["ampere", "volt", "watt", "percent"].includes(data.unit))))
53180
+ return false;
53181
+ if (kind === "faults" && !identifier(data.channelId)) return false;
53182
+ if (kind === "acknowledgements" && (!identifier(data.id) || !["accepted", "duplicate", "rejected"].includes(data.status)))
53183
+ return false;
53184
+ const oldest = [...this.controllers].find(([, value]) => !value.activeStream)?.[0];
53185
+ if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest === void 0) return false;
53186
+ if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest !== void 0)
53187
+ this.controllers.delete(oldest);
53188
+ const state = this.read(id);
53189
+ if (!canonical && kind === "heartbeat" && typeof data.sequence === "number") {
53190
+ if (data.sequence < (state.legacyHeartbeatSequence ?? 0)) return false;
53191
+ state.legacyHeartbeatSequence = data.sequence;
53192
+ }
53193
+ if (canonical) {
53194
+ if (kind === "measurements" && state.measurementAfter !== void 0 && sourceTime(data.timestamp) <= state.measurementAfter)
53195
+ return false;
53196
+ const previousStream = state.activeStream;
53197
+ const admission = admitEnvelope(state, data, kind, this.now());
53198
+ if (admission === "rejected") {
53199
+ if (state.trackingExhausted) {
53200
+ state.connected = false;
53201
+ this.controllers.set(id, state);
53202
+ }
53203
+ return false;
53204
+ }
53205
+ if (admission === "restart") {
53206
+ state.measurementAfter = sourceTime(data.timestamp) - (previousStream ? 0 : 1);
53207
+ state.inputs = /* @__PURE__ */ Object.create(null);
53208
+ state.outputs = /* @__PURE__ */ Object.create(null);
53209
+ state.measurements = /* @__PURE__ */ Object.create(null);
53210
+ state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
53211
+ state.connected = void 0;
53212
+ state.heartbeatAt = void 0;
53213
+ state.rejection = void 0;
53214
+ state.hardwareAvailable = void 0;
53215
+ state.stateSourceAt = void 0;
53216
+ state.revision = void 0;
53217
+ state.contentHash = void 0;
53218
+ }
53219
+ } else if (state.activeStream && !["heartbeat", "configuration/reported"].includes(kind)) {
53220
+ if (kind === "state" && data.connected === false) {
53221
+ state.connected = false;
53222
+ state.measurementAfter = this.now();
53223
+ state.stateSourceAt = void 0;
53224
+ this.controllers.set(id, state);
53225
+ }
53226
+ return false;
53227
+ }
53228
+ state.touched = this.now();
53229
+ const receivedAt = new Date(this.now()).toISOString();
53230
+ const metadata = {
53231
+ sourceAt: canonical ? data.timestamp : null,
53232
+ receivedAt,
53233
+ streamId: canonical ? state.activeStream : null,
53234
+ sequence: canonical ? data.sequence : null
53235
+ };
53236
+ if (kind === "heartbeat") state.heartbeatAt = canonical ? data.timestamp : receivedAt;
53237
+ if (kind === "state") {
53238
+ const contentHash = typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash) ? data.contentHash : void 0;
53239
+ const hardwareAvailable = isObject(data.readiness) && typeof data.readiness.hardwareAvailable === "boolean" ? data.readiness.hardwareAvailable : void 0;
53240
+ if (state.connected !== data.connected || state.revision !== data.revision || state.contentHash !== contentHash || state.hardwareAvailable !== hardwareAvailable) {
53241
+ if (canonical || state.stateSourceAt)
53242
+ state.measurementAfter = canonical ? sourceTime(data.timestamp) : this.now();
53243
+ state.measurements = /* @__PURE__ */ Object.create(null);
53244
+ state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
53245
+ }
53246
+ state.inputs = /* @__PURE__ */ Object.create(null);
53247
+ state.outputs = /* @__PURE__ */ Object.create(null);
53248
+ if (typeof data.connected === "boolean") state.connected = data.connected;
53249
+ state.revision = Number.isSafeInteger(data.revision) ? data.revision : void 0;
53250
+ state.contentHash = contentHash;
53251
+ state.hardwareAvailable = hardwareAvailable;
53252
+ state.stateSourceAt = canonical ? data.timestamp : void 0;
53253
+ if (data.outputs && typeof data.outputs === "object" && !Array.isArray(data.outputs)) {
53254
+ for (const [channelId, value] of Object.entries(data.outputs).slice(0, MAX_CHANNELS)) {
53255
+ if (identifier(channelId) && typeof value === "boolean")
53256
+ state.outputs[channelId] = { kind: "output", value, ...metadata };
53257
+ }
53258
+ }
53259
+ if (isObject(data.inputs))
53260
+ for (const [channelId, value] of Object.entries(data.inputs).slice(0, MAX_CHANNELS)) {
53261
+ if (identifier(channelId) && typeof value === "boolean")
53262
+ state.inputs[channelId] = { kind: "input", value, ...metadata };
53263
+ }
53264
+ }
53265
+ if (kind === "measurements" && identifier(data.channelId) && typeof data.value === "number" && Number.isFinite(data.value) && (canonical ? CANONICAL_UNITS : ["ampere", "volt", "watt", "percent"]).includes(data.unit)) {
53266
+ const collection2 = canonical && data.kind === "cumulative" ? state.cumulativeMeasurements : state.measurements;
53267
+ collection2[data.channelId] = {
53268
+ kind: "measurement",
53269
+ value: data.value,
53270
+ unit: data.unit,
53271
+ ...canonical ? { measurementKind: data.kind } : {},
53272
+ ...metadata
53273
+ };
53274
+ }
53275
+ if (kind === "faults" && identifier(data.channelId))
53276
+ state.faults[data.channelId] = {
53277
+ code: faultCodes.has(data.code) ? data.code : "runtime_fault",
53278
+ receivedAt
53279
+ };
53280
+ if (kind === "acknowledgements" && identifier(data.id) && ["accepted", "duplicate", "rejected"].includes(data.status)) {
53281
+ const command = this.commands.get(data.id);
53282
+ if (command?.controllerId === id) {
53283
+ state.acknowledgements[command.channelId] = {
53284
+ id: data.id,
53285
+ status: data.status,
53286
+ receivedAt
53287
+ };
53288
+ this.commands.delete(data.id);
53289
+ }
53290
+ }
53291
+ if (kind === "configuration/reported" && Number.isSafeInteger(data.revision) && typeof data.contentHash === "string")
53292
+ state.rejection = {
53293
+ revision: data.revision,
53294
+ contentHash: data.contentHash,
53295
+ receivedAt,
53296
+ errors: safeValidationSummaries(data.errors)
53297
+ };
53298
+ for (const collection2 of [
53299
+ state.cumulativeMeasurements,
53300
+ state.inputs,
53301
+ state.outputs,
53302
+ state.measurements,
53303
+ state.faults,
53304
+ state.acknowledgements
53305
+ ]) {
53306
+ while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
53307
+ }
53308
+ state.events.push({ kind, receivedAt });
53309
+ state.events = state.events.slice(-50);
53310
+ this.controllers.set(id, state);
53311
+ return true;
53312
+ }
53313
+ };
53314
+
52737
53315
  // backend/wago.service.ts
52738
53316
  var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
52739
53317
  var STALE_AFTER_MS = 9e4;
@@ -52748,6 +53326,7 @@ var MqttSubscriptionError = class extends Error {
52748
53326
  var WagoService = class {
52749
53327
  constructor(context) {
52750
53328
  __publicField(this, "context", context);
53329
+ __publicField(this, "diagnostics", new WagoDiagnosticsStore());
52751
53330
  __publicField(this, "controllers");
52752
53331
  __publicField(this, "settings");
52753
53332
  __publicField(this, "enrollments");
@@ -52765,7 +53344,9 @@ var WagoService = class {
52765
53344
  controllers: () => this.controllers,
52766
53345
  claimedController: (id) => this.claimedController(id),
52767
53346
  getSettings: () => this.getSettings(),
52768
- appliedRevision: (id) => this.appliedRevision(id)
53347
+ appliedRevision: (id) => this.appliedRevision(id),
53348
+ onCommand: (controllerId, channelId, id) => this.diagnostics.command(controllerId, channelId, id),
53349
+ onCommandFailure: (id, status) => this.diagnostics.commandFailed(id, status)
52769
53350
  }));
52770
53351
  __publicField(this, "claimConfigurationLock", Promise.resolve());
52771
53352
  __publicField(this, "subscriptionRebuild", Promise.resolve());
@@ -52829,7 +53410,7 @@ var WagoService = class {
52829
53410
  runtimeVersion: controller.runtimeVersion,
52830
53411
  capabilities: controller.capabilities,
52831
53412
  lastSequence: controller.lastSequence,
52832
- lastHeartbeatAt: controller.lastHeartbeatAt,
53413
+ lastHeartbeatAt: this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt,
52833
53414
  lastSeenAt: controller.lastSeenAt,
52834
53415
  compatibilityError: controller.compatibilityError,
52835
53416
  createdAt: controller.createdAt,
@@ -52875,13 +53456,40 @@ var WagoService = class {
52875
53456
  await this.claimedController(controllerId);
52876
53457
  return this.drafts.findOneBy({ controllerId });
52877
53458
  }
52878
- async saveDraft(controllerId, snapshot) {
52879
- return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot));
53459
+ async saveDraft(controllerId, snapshot, metadata) {
53460
+ return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot, metadata));
52880
53461
  }
52881
- async validateDraft(controllerId) {
53462
+ presets() {
53463
+ return WAGO_PRESETS;
53464
+ }
53465
+ async previewPreset(controllerId, application, snapshot) {
52882
53466
  const draft = await this.getDraft(controllerId);
52883
- if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
52884
- 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);
52885
53493
  return { valid: errors.length === 0, errors };
52886
53494
  }
52887
53495
  async revisionsFor(controllerId, offset = 0, limit = 20) {
@@ -52899,7 +53507,8 @@ var WagoService = class {
52899
53507
  "state",
52900
53508
  "rejectionErrors",
52901
53509
  "publishedAt",
52902
- "reportedAt"
53510
+ "reportedAt",
53511
+ "presetProvenance"
52903
53512
  ],
52904
53513
  skip: pageOffset,
52905
53514
  take: pageLimit
@@ -52909,35 +53518,93 @@ var WagoService = class {
52909
53518
  async reviewDraft(controllerId) {
52910
53519
  return this.withConfigurationLock(controllerId, () => this.reviewDraftWhileLocked(controllerId));
52911
53520
  }
52912
- async publishDraft(controllerId) {
52913
- return this.withConfigurationLock(controllerId, () => this.publishDraftWhileLocked(controllerId));
53521
+ async publishDraft(controllerId, force = false, reviewedHash) {
53522
+ return this.withConfigurationLock(
53523
+ controllerId,
53524
+ () => this.publishDraftWhileLocked(controllerId, force, reviewedHash)
53525
+ );
52914
53526
  }
52915
- async rollback(controllerId, revision) {
53527
+ async rollback(controllerId, revision, force = false, sourceHash, currentHash, draftHash, impactHash) {
52916
53528
  return this.withConfigurationLock(controllerId, async () => {
53529
+ await this.claimedController(controllerId);
52917
53530
  const source = await this.revisions.findOneBy({ controllerId, revision });
52918
53531
  if (!source) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
52919
- await this.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
+ );
52920
53545
  await this.reviewDraftWhileLocked(controllerId);
52921
- return this.publishDraftWhileLocked(controllerId);
53546
+ return this.publishDraftWhileLocked(controllerId, force);
52922
53547
  });
52923
53548
  }
52924
53549
  async previewRevision(controllerId, revision) {
52925
- await this.claimedController(controllerId);
52926
- const selected = await this.revisions.findOneBy({ controllerId, revision });
52927
- if (!selected) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
52928
- const [current] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
52929
- return {
52930
- revision: selected,
52931
- current: current ?? null,
52932
- diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, JSON.parse(selected.snapshot))
52933
- };
53550
+ return this.withConfigurationLock(controllerId, async () => {
53551
+ await this.claimedController(controllerId);
53552
+ const selected = await this.revisions.findOneBy({ controllerId, revision });
53553
+ if (!selected) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
53554
+ const [current] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
53555
+ const draft = await this.drafts.findOneBy({ controllerId });
53556
+ const impacts = await configurationFlowImpacts(
53557
+ this.context,
53558
+ controllerId,
53559
+ current ? JSON.parse(current.snapshot) : null,
53560
+ JSON.parse(selected.snapshot)
53561
+ );
53562
+ return {
53563
+ draftHash: this.draftIdentity(draft),
53564
+ impacts,
53565
+ revision: selected,
53566
+ current: current ?? null,
53567
+ diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, JSON.parse(selected.snapshot)),
53568
+ metadataDiff: configurationDiff(
53569
+ this.metadataFromProvenance(current?.presetProvenance),
53570
+ this.metadataFromProvenance(selected.presetProvenance)
53571
+ ),
53572
+ impactHash: configurationHash({ impacts })
53573
+ };
53574
+ });
53575
+ }
53576
+ draftIdentity(draft) {
53577
+ return configurationHash(draft ? { snapshot: draft.snapshot, metadata: draft.presetProvenance ?? null } : null);
53578
+ }
53579
+ revisionIdentity(revision) {
53580
+ return revision ? `${revision.revision}:${revision.contentHash}` : null;
52934
53581
  }
52935
- async saveDraftWhileLocked(controllerId, snapshot) {
53582
+ reviewIdentity(draft, impacts) {
53583
+ return configurationHash({ draft: this.draftIdentity(draft), impacts });
53584
+ }
53585
+ metadataFromProvenance(provenance) {
53586
+ if (!provenance) return { names: {}, presets: [] };
53587
+ try {
53588
+ return editorMetadata(JSON.parse(provenance).editor);
53589
+ } catch {
53590
+ return { names: {}, presets: [] };
53591
+ }
53592
+ }
53593
+ async saveDraftWhileLocked(controllerId, snapshot, metadata) {
52936
53594
  await this.claimedController(controllerId);
53595
+ let provenance;
53596
+ if (metadata !== void 0) {
53597
+ try {
53598
+ provenance = JSON.stringify({ editor: editorMetadata(metadata) });
53599
+ } catch (error) {
53600
+ throw new import_common.BadRequestException(error instanceof Error ? error.message : "invalid editor metadata");
53601
+ }
53602
+ }
52937
53603
  const serialized = canonicalSnapshot(snapshot);
52938
53604
  const existing = await this.drafts.findOneBy({ controllerId });
52939
53605
  const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, updatedAt: "" });
52940
53606
  draft.snapshot = serialized;
53607
+ if (provenance !== void 0) draft.presetProvenance = provenance;
52941
53608
  draft.reviewedHash = null;
52942
53609
  draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
52943
53610
  return this.drafts.save(draft);
@@ -52947,28 +53614,66 @@ var WagoService = class {
52947
53614
  const draft = await this.drafts.findOneBy({ controllerId });
52948
53615
  if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
52949
53616
  const previous = await this.latestRevision(controllerId);
52950
- 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);
52951
53624
  await this.drafts.save(draft);
52952
53625
  const diff = configurationDiff(previous ? JSON.parse(previous.snapshot) : null, JSON.parse(draft.snapshot));
52953
- 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
+ };
52954
53638
  }
52955
- 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) {
52956
53656
  const controller = await this.claimedController(controllerId);
52957
53657
  const draft = await this.drafts.findOneBy({ controllerId });
52958
53658
  if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
52959
- const validation = validateSnapshot(JSON.parse(draft.snapshot));
53659
+ const validation = validateEditorSnapshot(JSON.parse(draft.snapshot));
52960
53660
  if (validation.length)
52961
53661
  throw new import_common.ConflictException({ message: "configuration draft is invalid", errors: validation });
52962
53662
  const contentHash = configurationHash(JSON.parse(draft.snapshot));
52963
- if (draft.reviewedHash !== contentHash)
52964
- throw new import_common.ConflictException("review the current configuration draft before publishing it");
52965
53663
  const previous = await this.latestRevision(controllerId);
52966
- 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))
52967
53671
  return this.publishRevision(controller, previous);
52968
53672
  const revision = this.revisions.create({
52969
53673
  controllerId,
52970
53674
  revision: (previous?.revision ?? 0) + 1,
52971
53675
  snapshot: draft.snapshot,
53676
+ presetProvenance: draft.presetProvenance ?? null,
52972
53677
  contentHash,
52973
53678
  state: "pending",
52974
53679
  rejectionErrors: null,
@@ -53284,6 +53989,7 @@ var WagoService = class {
53284
53989
  });
53285
53990
  const generation = this.activeSubscriptionGeneration + 1;
53286
53991
  const replacements = [];
53992
+ const retainedStates = /* @__PURE__ */ new Map();
53287
53993
  try {
53288
53994
  for (const serverId of serverIds) {
53289
53995
  replacements.push(
@@ -53310,7 +54016,10 @@ var WagoService = class {
53310
54016
  if (!this.isActiveSubscriptionGeneration(generation)) return;
53311
54017
  const hardwareId = configurationReportedHardwareId(settings.operationalPrefix, message.topic);
53312
54018
  const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
53313
- if (controller) 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
+ }
53314
54023
  }
53315
54024
  )
53316
54025
  );
@@ -53319,7 +54028,10 @@ var WagoService = class {
53319
54028
  if (!this.isActiveSubscriptionGeneration(generation)) return;
53320
54029
  const hardwareId = acknowledgementHardwareId(settings.operationalPrefix, message.topic);
53321
54030
  const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
53322
- if (controller) 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
+ }
53323
54035
  })
53324
54036
  );
53325
54037
  if (this.destroyed) {
@@ -53327,6 +54039,20 @@ var WagoService = class {
53327
54039
  return;
53328
54040
  }
53329
54041
  for (const controller of claimedControllers) {
54042
+ for (const suffix of ["state", "measurements", "faults"]) {
54043
+ replacements.push(
54044
+ await this.subscribeMqtt(
54045
+ serverId,
54046
+ `${settings.operationalPrefix}/v1/controllers/${controller.hardwareId}/${suffix}`,
54047
+ (message) => {
54048
+ if (this.isActiveSubscriptionGeneration(generation))
54049
+ this.diagnostics.ingest(controller.id, suffix, message.payload);
54050
+ else if (!this.destroyed && suffix === "state" && message.payload.length <= 65536)
54051
+ retainedStates.set(controller.id, Buffer.from(message.payload));
54052
+ }
54053
+ )
54054
+ );
54055
+ }
53330
54056
  replacements.push(
53331
54057
  await this.subscribeMqtt(
53332
54058
  serverId,
@@ -53352,6 +54078,7 @@ var WagoService = class {
53352
54078
  return;
53353
54079
  }
53354
54080
  this.activeSubscriptionGeneration = generation;
54081
+ retainedStates.forEach((payload, controllerId) => this.diagnostics.ingest(controllerId, "state", payload));
53355
54082
  this.unsubscribe();
53356
54083
  this.subscriptions.push(...replacements);
53357
54084
  }
@@ -53443,15 +54170,35 @@ var WagoService = class {
53443
54170
  return;
53444
54171
  }
53445
54172
  if (heartbeat.hardwareId !== hardwareId) return;
54173
+ let rawHeartbeat;
54174
+ try {
54175
+ rawHeartbeat = JSON.parse(payload.toString("utf8"));
54176
+ } catch {
54177
+ return;
54178
+ }
54179
+ const canonical = canonicalEnvelope(rawHeartbeat, "heartbeat");
53446
54180
  const controller = await this.controllers.findOneBy({ hardwareId });
53447
- if (!controller || controller.trustState !== "claimed" || heartbeat.sequence !== void 0 && heartbeat.sequence < controller.lastSequence)
54181
+ if (!controller || controller.trustState !== "claimed" || !canonical && heartbeat.sequence !== void 0 && heartbeat.sequence < controller.lastSequence)
53448
54182
  return;
53449
54183
  const now = (/* @__PURE__ */ new Date()).toISOString();
54184
+ const canTrackDiagnostics = this.diagnostics.canTrack(controller.id);
54185
+ const admitted = this.diagnostics.ingest(controller.id, "heartbeat", payload);
54186
+ const heartbeatAt = admitted ? this.diagnostics.read(controller.id).heartbeatAt : typeof rawHeartbeat.timestamp === "string" ? rawHeartbeat.timestamp : void 0;
54187
+ const persistedHeartbeatAt = sourceTime(controller.lastHeartbeatAt);
54188
+ if (canonical && (!validEnvelope(rawHeartbeat, Date.now()) || canTrackDiagnostics && !admitted || !heartbeatAt || persistedHeartbeatAt !== null && sourceTime(heartbeatAt) < persistedHeartbeatAt))
54189
+ return;
54190
+ const metadataChanged = controller.protocolVersion !== heartbeat.protocolVersion || controller.runtimeVersion !== heartbeat.runtimeVersion || controller.capabilities !== JSON.stringify(heartbeat.capabilities) || controller.compatibilityError !== compatibilityError(heartbeat);
54191
+ if (controller.lastHeartbeatAt && freshness(controller.lastSeenAt, Date.now(), 3e4) === "fresh" && !metadataChanged)
54192
+ return;
53450
54193
  controller.protocolVersion = heartbeat.protocolVersion;
53451
54194
  controller.runtimeVersion = heartbeat.runtimeVersion;
53452
54195
  controller.capabilities = JSON.stringify(heartbeat.capabilities);
53453
- controller.lastSequence = heartbeat.sequence ?? controller.lastSequence;
53454
- 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
+ }
53455
54202
  controller.lastSeenAt = now;
53456
54203
  controller.compatibilityError = compatibilityError(heartbeat);
53457
54204
  controller.updatedAt = now;
@@ -53562,7 +54309,8 @@ var WagoService = class {
53562
54309
  }
53563
54310
  connectivity(controller) {
53564
54311
  if (controller.trustState === "untrusted") return "untrusted";
53565
- 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";
53566
54314
  }
53567
54315
  async appliedRevision(controllerId) {
53568
54316
  const [revision] = await this.revisions.find({
@@ -53734,10 +54482,10 @@ var import_node_fs2 = require("node:fs");
53734
54482
  var import_promises4 = require("node:fs/promises");
53735
54483
  var import_node_os2 = require("node:os");
53736
54484
  var import_node_path2 = require("node:path");
53737
- var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
54485
+ var import_plugins_backend_sdk12 = require("@attraccess/plugins-backend-sdk");
53738
54486
 
53739
54487
  // backend/wago-commissioning-session.entity.ts
53740
- var import_plugins_backend_sdk7 = require("@attraccess/plugins-backend-sdk");
54488
+ var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
53741
54489
  var WagoCommissioningSession = class {
53742
54490
  constructor() {
53743
54491
  __publicField(this, "id");
@@ -53769,85 +54517,85 @@ var WagoCommissioningSession = class {
53769
54517
  }
53770
54518
  };
53771
54519
  __decorateClass([
53772
- (0, import_plugins_backend_sdk7.PrimaryGeneratedColumn)({ type: "integer" })
54520
+ (0, import_plugins_backend_sdk8.PrimaryGeneratedColumn)({ type: "integer" })
53773
54521
  ], WagoCommissioningSession.prototype, "id", 2);
53774
54522
  __decorateClass([
53775
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "hardware_id" })
54523
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "hardware_id" })
53776
54524
  ], WagoCommissioningSession.prototype, "hardwareId", 2);
53777
54525
  __decorateClass([
53778
- (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" })
53779
54527
  ], WagoCommissioningSession.prototype, "mqttServerId", 2);
53780
54528
  __decorateClass([
53781
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "target_host" })
54529
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "target_host" })
53782
54530
  ], WagoCommissioningSession.prototype, "targetHost", 2);
53783
54531
  __decorateClass([
53784
- (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" })
53785
54533
  ], WagoCommissioningSession.prototype, "hostKeyFingerprint", 2);
53786
54534
  __decorateClass([
53787
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "firmware_baseline" })
54535
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "firmware_baseline" })
53788
54536
  ], WagoCommissioningSession.prototype, "firmwareBaseline", 2);
53789
54537
  __decorateClass([
53790
- (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 })
53791
54539
  ], WagoCommissioningSession.prototype, "controllerName", 2);
53792
54540
  __decorateClass([
53793
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar" })
54541
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar" })
53794
54542
  ], WagoCommissioningSession.prototype, "state", 2);
53795
54543
  __decorateClass([
53796
- (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 })
53797
54545
  ], WagoCommissioningSession.prototype, "enrollmentExpiresAt", 2);
53798
54546
  __decorateClass([
53799
- (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 })
53800
54548
  ], WagoCommissioningSession.prototype, "enrollmentId", 2);
53801
54549
  __decorateClass([
53802
- (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 })
53803
54551
  ], WagoCommissioningSession.prototype, "pairingCode", 2);
53804
54552
  __decorateClass([
53805
- (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 })
53806
54554
  ], WagoCommissioningSession.prototype, "deliveryToken", 2);
53807
54555
  __decorateClass([
53808
- (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 })
53809
54557
  ], WagoCommissioningSession.prototype, "initiatingPrincipal", 2);
53810
54558
  __decorateClass([
53811
- (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 })
53812
54560
  ], WagoCommissioningSession.prototype, "runtimeArtifactDigest", 2);
53813
54561
  __decorateClass([
53814
- (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 })
53815
54563
  ], WagoCommissioningSession.prototype, "managementControllerId", 2);
53816
54564
  __decorateClass([
53817
- (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 })
53818
54566
  ], WagoCommissioningSession.prototype, "dockerProvisionToken", 2);
53819
54567
  __decorateClass([
53820
- (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 })
53821
54569
  ], WagoCommissioningSession.prototype, "dockerProvisionState", 2);
53822
54570
  __decorateClass([
53823
- (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 })
53824
54572
  ], WagoCommissioningSession.prototype, "platformReport", 2);
53825
54573
  __decorateClass([
53826
- (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 })
53827
54575
  ], WagoCommissioningSession.prototype, "codesysState", 2);
53828
54576
  __decorateClass([
53829
- (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 })
53830
54578
  ], WagoCommissioningSession.prototype, "progressPercent", 2);
53831
54579
  __decorateClass([
53832
- (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 })
53833
54581
  ], WagoCommissioningSession.prototype, "progressStep", 2);
53834
54582
  __decorateClass([
53835
- (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 })
53836
54584
  ], WagoCommissioningSession.prototype, "progressDetail", 2);
53837
54585
  __decorateClass([
53838
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "audit_log" })
54586
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "audit_log" })
53839
54587
  ], WagoCommissioningSession.prototype, "auditLog", 2);
53840
54588
  __decorateClass([
53841
- (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 })
53842
54590
  ], WagoCommissioningSession.prototype, "failureReason", 2);
53843
54591
  __decorateClass([
53844
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "created_at" })
54592
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "created_at" })
53845
54593
  ], WagoCommissioningSession.prototype, "createdAt", 2);
53846
54594
  __decorateClass([
53847
- (0, import_plugins_backend_sdk7.Column)({ type: "varchar", name: "updated_at" })
54595
+ (0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "updated_at" })
53848
54596
  ], WagoCommissioningSession.prototype, "updatedAt", 2);
53849
54597
  WagoCommissioningSession = __decorateClass([
53850
- (0, import_plugins_backend_sdk7.Entity)({ name: "plugin_wago_commissioning_sessions" })
54598
+ (0, import_plugins_backend_sdk8.Entity)({ name: "plugin_wago_commissioning_sessions" })
53851
54599
  ], WagoCommissioningSession);
53852
54600
 
53853
54601
  // backend/wago-commissioning-verification.ts
@@ -54245,13 +54993,13 @@ async function verifyRuntimeSignature(file, armor, trustedKey = WAGO_RUNTIME_REL
54245
54993
  if (!match) invalid();
54246
54994
  const packet = Buffer.from(match[1].replace(/\s/g, ""), "base64");
54247
54995
  if (packet.length < 10 || packet.subarray(0, 6).toString() !== "SSHSIG" || packet.readUInt32BE(6) !== 1) invalid();
54248
- const fields = reader(packet.subarray(10));
54249
- const key = fields.string();
54250
- const namespace = fields.string();
54251
- const reserved = fields.string();
54252
- const algorithm2 = fields.string();
54253
- const signature = reader(fields.string());
54254
- 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();
54255
55003
  if (!key.equals(Buffer.from(trustedKey, "base64")) || namespace.toString() !== "attraccess-wago-runtime" || reserved.length)
54256
55004
  invalid();
54257
55005
  if (!["sha256", "sha512"].includes(algorithm2.toString()) || signature.string().toString() !== "ssh-ed25519")
@@ -54716,7 +55464,7 @@ WagoRuntimeArtifactsService = __decorateClass([
54716
55464
 
54717
55465
  // backend/wago-commissioning-readiness.ts
54718
55466
  var import_common5 = require("@nestjs/common");
54719
- var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
55467
+ var import_plugins_backend_sdk9 = require("@attraccess/plugins-backend-sdk");
54720
55468
  var WagoCommissioningReadiness = class {
54721
55469
  constructor(context) {
54722
55470
  __publicField(this, "context", context);
@@ -54780,14 +55528,14 @@ var WagoCommissioningReadiness = class {
54780
55528
  };
54781
55529
  WagoCommissioningReadiness = __decorateClass([
54782
55530
  (0, import_common5.Injectable)(),
54783
- __decorateParam(0, (0, import_common5.Inject)(import_plugins_backend_sdk8.PLUGIN_CONTEXT))
55531
+ __decorateParam(0, (0, import_common5.Inject)(import_plugins_backend_sdk9.PLUGIN_CONTEXT))
54784
55532
  ], WagoCommissioningReadiness);
54785
55533
 
54786
55534
  // backend/wago-commissioning-lease.ts
54787
55535
  var import_node_crypto8 = require("node:crypto");
54788
55536
 
54789
55537
  // backend/wago-commissioning-lease.entity.ts
54790
- var import_plugins_backend_sdk9 = require("@attraccess/plugins-backend-sdk");
55538
+ var import_plugins_backend_sdk10 = require("@attraccess/plugins-backend-sdk");
54791
55539
  var WagoCommissioningLeaseEntity = class {
54792
55540
  constructor() {
54793
55541
  __publicField(this, "fingerprintHash");
@@ -54798,22 +55546,22 @@ var WagoCommissioningLeaseEntity = class {
54798
55546
  }
54799
55547
  };
54800
55548
  __decorateClass([
54801
- (0, 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 })
54802
55550
  ], WagoCommissioningLeaseEntity.prototype, "fingerprintHash", 2);
54803
55551
  __decorateClass([
54804
- (0, import_plugins_backend_sdk9.Column)({ type: "varchar", name: "owner" })
55552
+ (0, import_plugins_backend_sdk10.Column)({ type: "varchar", name: "owner" })
54805
55553
  ], WagoCommissioningLeaseEntity.prototype, "owner", 2);
54806
55554
  __decorateClass([
54807
- (0, import_plugins_backend_sdk9.Column)({ type: "bigint", name: "lease_until" })
55555
+ (0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "lease_until" })
54808
55556
  ], WagoCommissioningLeaseEntity.prototype, "leaseUntil", 2);
54809
55557
  __decorateClass([
54810
- (0, import_plugins_backend_sdk9.Column)({ type: "bigint", name: "operation_until" })
55558
+ (0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "operation_until" })
54811
55559
  ], WagoCommissioningLeaseEntity.prototype, "operationUntil", 2);
54812
55560
  __decorateClass([
54813
- (0, import_plugins_backend_sdk9.Column)({ type: "bigint", name: "recovery_after" })
55561
+ (0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "recovery_after" })
54814
55562
  ], WagoCommissioningLeaseEntity.prototype, "recoveryAfter", 2);
54815
55563
  WagoCommissioningLeaseEntity = __decorateClass([
54816
- (0, import_plugins_backend_sdk9.Entity)({ name: "plugin_wago_commissioning_lease" })
55564
+ (0, import_plugins_backend_sdk10.Entity)({ name: "plugin_wago_commissioning_lease" })
54817
55565
  ], WagoCommissioningLeaseEntity);
54818
55566
 
54819
55567
  // backend/wago-commissioning-lease.ts
@@ -54989,7 +55737,7 @@ function createWagoCommissioningLeaseService(context) {
54989
55737
  }
54990
55738
 
54991
55739
  // backend/wago-management.entity.ts
54992
- var import_plugins_backend_sdk10 = require("@attraccess/plugins-backend-sdk");
55740
+ var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
54993
55741
  var WagoManagementEntity = class {
54994
55742
  constructor() {
54995
55743
  __publicField(this, "controllerId");
@@ -55000,22 +55748,22 @@ var WagoManagementEntity = class {
55000
55748
  }
55001
55749
  };
55002
55750
  __decorateClass([
55003
- (0, import_plugins_backend_sdk10.PrimaryColumn)({ type: "integer", name: "controller_id" })
55751
+ (0, import_plugins_backend_sdk11.PrimaryColumn)({ type: "integer", name: "controller_id" })
55004
55752
  ], WagoManagementEntity.prototype, "controllerId", 2);
55005
55753
  __decorateClass([
55006
- (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 })
55007
55755
  ], WagoManagementEntity.prototype, "metadataJson", 2);
55008
55756
  __decorateClass([
55009
- (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 })
55010
55758
  ], WagoManagementEntity.prototype, "encryptedPrivateKey", 2);
55011
55759
  __decorateClass([
55012
- (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 })
55013
55761
  ], WagoManagementEntity.prototype, "leaseOwner", 2);
55014
55762
  __decorateClass([
55015
- (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 })
55016
55764
  ], WagoManagementEntity.prototype, "leaseUntil", 2);
55017
55765
  WagoManagementEntity = __decorateClass([
55018
- (0, import_plugins_backend_sdk10.Entity)({ name: "plugin_wago_management" })
55766
+ (0, import_plugins_backend_sdk11.Entity)({ name: "plugin_wago_management" })
55019
55767
  ], WagoManagementEntity);
55020
55768
 
55021
55769
  // backend/wago-management-inspection.ts
@@ -55480,7 +56228,7 @@ var transitionStates = [
55480
56228
  "recovery_required"
55481
56229
  ];
55482
56230
  var exceptionNames = ["wbm_exposed", "other_services_exposed", "unqualified_privileges"];
55483
- var identifier = () => (0, import_node_crypto10.randomBytes)(16).toString("hex");
56231
+ var identifier2 = () => (0, import_node_crypto10.randomBytes)(16).toString("hex");
55484
56232
  var REVIEW_MS = 3e5;
55485
56233
  var LEASE_MS = 30 * 6e4;
55486
56234
  var ManagementError = class extends Error {
@@ -55547,7 +56295,7 @@ var WagoManagementService = class {
55547
56295
  record3.support = this.adapter.qualify(record3.inspection, input.mode).support;
55548
56296
  if (input.mode === "key_only" && (!record3.exceptions.includes("unqualified_privileges") || record3.inspection.wbm !== "not_observed" && !record3.exceptions.includes("wbm_exposed") || record3.inspection.otherManagement !== "not_observed" && !record3.exceptions.includes("other_services_exposed")))
55549
56297
  throw new ManagementError("invalid_request");
55550
- record3.reviewToken = identifier();
56298
+ record3.reviewToken = identifier2();
55551
56299
  record3.reviewedAt = this.now();
55552
56300
  record3.state = "reviewed";
55553
56301
  await this.save(record3, owner, guard);
@@ -55580,7 +56328,7 @@ var WagoManagementService = class {
55580
56328
  if (this.secrets.decrypt(record2.encryptedPrivateKey) !== key.privateKey) throw new Error();
55581
56329
  record2.keyFingerprint = key.fingerprint;
55582
56330
  record2.transaction = {
55583
- id: identifier(),
56331
+ id: identifier2(),
55584
56332
  target: record2.target,
55585
56333
  username: input.temporarySsh.username,
55586
56334
  deadline: this.now() + 15e4
@@ -55636,7 +56384,7 @@ var WagoManagementService = class {
55636
56384
  });
55637
56385
  }
55638
56386
  async verify(record2, privateKey, guard) {
55639
- const nonce = identifier();
56387
+ const nonce = identifier2();
55640
56388
  const tx = this.transaction(record2);
55641
56389
  const proof = await this.remote(guard, () => this.adapter.verifyKey(tx, privateKey, nonce));
55642
56390
  if (proof.nonce !== nonce || !proof.keyOnly || proof.hostKeyFingerprint !== tx.target.hostKeyFingerprint || proof.keyFingerprint !== record2.keyFingerprint || !Number.isSafeInteger(proof.uid) || proof.uid <= 0 || proof.uid !== this.inspection(record2).uid || !proof.managementOperationSucceeded)
@@ -55708,7 +56456,7 @@ var WagoManagementService = class {
55708
56456
  }
55709
56457
  async locked(controllerId, fingerprint2, action) {
55710
56458
  validId(controllerId);
55711
- const owner = identifier();
56459
+ const owner = identifier2();
55712
56460
  let acquired = false;
55713
56461
  let mutationStarted = false;
55714
56462
  try {
@@ -57319,7 +58067,7 @@ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
57319
58067
  };
57320
58068
  WagoCommissioningService = __decorateClass([
57321
58069
  (0, import_common6.Injectable)(),
57322
- __decorateParam(0, (0, import_common6.Inject)(import_plugins_backend_sdk11.PLUGIN_CONTEXT)),
58070
+ __decorateParam(0, (0, import_common6.Inject)(import_plugins_backend_sdk12.PLUGIN_CONTEXT)),
57323
58071
  __decorateParam(1, (0, import_common6.Inject)(WagoService)),
57324
58072
  __decorateParam(2, (0, import_common6.Optional)()),
57325
58073
  __decorateParam(2, (0, import_common6.Inject)(WagoRuntimeArtifactsService)),
@@ -57754,17 +58502,23 @@ var WagoControllerApi = class {
57754
58502
  }
57755
58503
  previewPreset(id, body) {
57756
58504
  if (!body?.application) throw new import_common8.BadRequestException("application is required");
57757
- return this.wago.previewPreset(id, body.application);
58505
+ return this.wago.previewPreset(id, body.application, body.snapshot);
57758
58506
  }
57759
58507
  applyPreset(id, body) {
57760
58508
  if (!body?.application) throw new import_common8.BadRequestException("application is required");
57761
- return this.wago.applyPreset(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
+ );
57762
58516
  }
57763
58517
  saveDraft(id, body) {
57764
- return this.wago.saveDraft(id, body?.snapshot);
58518
+ return this.wago.saveDraft(id, body?.snapshot, body?.metadata);
57765
58519
  }
57766
- validateDraft(id) {
57767
- return this.wago.validateDraft(id);
58520
+ validateDraft(id, body) {
58521
+ return this.wago.validateDraft(id, body?.snapshot);
57768
58522
  }
57769
58523
  reviewDraft(id) {
57770
58524
  return this.wago.reviewDraft(id);
@@ -57772,23 +58526,31 @@ var WagoControllerApi = class {
57772
58526
  revisions(id, offset, limit) {
57773
58527
  return this.wago.revisionsFor(id, Number(offset), Number(limit));
57774
58528
  }
57775
- publishDraft(id, request) {
58529
+ publishDraft(id, request, body) {
57776
58530
  return this.audit.run(
57777
58531
  wagoAuditPrincipal(request),
57778
58532
  id,
57779
58533
  "publication",
57780
- {},
57781
- () => this.wago.publishDraft(id),
58534
+ body?.force === true ? { force: true } : {},
58535
+ () => this.wago.publishDraft(id, body?.force === true, body?.reviewedHash),
57782
58536
  (published) => ({ revision: published.revision })
57783
58537
  );
57784
58538
  }
57785
- rollback(id, revision, request) {
58539
+ rollback(id, revision, request, body) {
57786
58540
  return this.audit.run(
57787
58541
  wagoAuditPrincipal(request),
57788
58542
  id,
57789
58543
  "rollback",
57790
- { sourceRevision: revision },
57791
- () => this.wago.rollback(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
+ ),
57792
58554
  (published) => ({ revision: published.revision })
57793
58555
  );
57794
58556
  }
@@ -57800,61 +58562,61 @@ __decorateClass([
57800
58562
  (0, import_common8.Get)("controllers")
57801
58563
  ], WagoControllerApi.prototype, "list", 1);
57802
58564
  __decorateClass([
57803
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58565
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57804
58566
  (0, import_common8.Get)("settings")
57805
58567
  ], WagoControllerApi.prototype, "settings", 1);
57806
58568
  __decorateClass([
57807
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58569
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57808
58570
  (0, import_common8.Post)("settings"),
57809
58571
  __decorateParam(0, (0, import_common8.Body)())
57810
58572
  ], WagoControllerApi.prototype, "setSettings", 1);
57811
58573
  __decorateClass([
57812
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58574
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57813
58575
  (0, import_common8.Get)("commissioning/support")
57814
58576
  ], WagoControllerApi.prototype, "commissioningSupport", 1);
57815
58577
  __decorateClass([
57816
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58578
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57817
58579
  (0, import_common8.Get)("commissioning/sessions"),
57818
58580
  __decorateParam(0, (0, import_common8.Query)("limit")),
57819
58581
  __decorateParam(1, (0, import_common8.Query)("offset"))
57820
58582
  ], WagoControllerApi.prototype, "commissioningSessions", 1);
57821
58583
  __decorateClass([
57822
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58584
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57823
58585
  (0, import_common8.Post)("commissioning/sessions"),
57824
58586
  __decorateParam(0, (0, import_common8.Body)())
57825
58587
  ], WagoControllerApi.prototype, "createCommissioningSession", 1);
57826
58588
  __decorateClass([
57827
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58589
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57828
58590
  (0, import_common8.Post)("commissioning/sessions/:id/confirm-host-key"),
57829
58591
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57830
58592
  __decorateParam(1, (0, import_common8.Body)())
57831
58593
  ], WagoControllerApi.prototype, "confirmCommissioningHostKey", 1);
57832
58594
  __decorateClass([
57833
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58595
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57834
58596
  (0, import_common8.Post)("commissioning/sessions/:id/deliver"),
57835
58597
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57836
58598
  __decorateParam(1, (0, import_common8.Body)()),
57837
58599
  __decorateParam(2, (0, import_common8.Req)())
57838
58600
  ], WagoControllerApi.prototype, "deliverCommissioningSession", 1);
57839
58601
  __decorateClass([
57840
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58602
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57841
58603
  (0, import_common8.Post)("commissioning/sessions/:id/recover"),
57842
58604
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57843
58605
  __decorateParam(1, (0, import_common8.Body)()),
57844
58606
  __decorateParam(2, (0, import_common8.Req)())
57845
58607
  ], WagoControllerApi.prototype, "recoverCommissioningSession", 1);
57846
58608
  __decorateClass([
57847
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58609
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57848
58610
  (0, import_common8.Get)("commissioning/sessions/:id/verification"),
57849
58611
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
57850
58612
  ], WagoControllerApi.prototype, "commissioningVerification", 1);
57851
58613
  __decorateClass([
57852
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58614
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57853
58615
  (0, import_common8.Post)("commissioning/sessions/:id/revoke"),
57854
58616
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
57855
58617
  ], WagoControllerApi.prototype, "revokeCommissioningSession", 1);
57856
58618
  __decorateClass([
57857
- (0, import_plugins_backend_sdk12.Auth)("system.settings.manage"),
58619
+ (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
57858
58620
  (0, import_common8.Delete)("commissioning/sessions/:id"),
57859
58621
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
57860
58622
  ], WagoControllerApi.prototype, "removeCommissioningSession", 1);
@@ -57893,7 +58655,8 @@ __decorateClass([
57893
58655
  ], WagoControllerApi.prototype, "saveDraft", 1);
57894
58656
  __decorateClass([
57895
58657
  (0, import_common8.Post)("controllers/:id/configuration/validate"),
57896
- __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
58658
+ __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
58659
+ __decorateParam(1, (0, import_common8.Body)())
57897
58660
  ], WagoControllerApi.prototype, "validateDraft", 1);
57898
58661
  __decorateClass([
57899
58662
  (0, import_common8.Post)("controllers/:id/configuration/review"),
@@ -57908,13 +58671,15 @@ __decorateClass([
57908
58671
  __decorateClass([
57909
58672
  (0, import_common8.Post)("controllers/:id/configuration/publish"),
57910
58673
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57911
- __decorateParam(1, (0, import_common8.Req)())
58674
+ __decorateParam(1, (0, import_common8.Req)()),
58675
+ __decorateParam(2, (0, import_common8.Body)())
57912
58676
  ], WagoControllerApi.prototype, "publishDraft", 1);
57913
58677
  __decorateClass([
57914
58678
  (0, import_common8.Post)("controllers/:id/configuration/rollback/:revision"),
57915
58679
  __decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
57916
58680
  __decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe)),
57917
- __decorateParam(2, (0, import_common8.Req)())
58681
+ __decorateParam(2, (0, import_common8.Req)()),
58682
+ __decorateParam(3, (0, import_common8.Body)())
57918
58683
  ], WagoControllerApi.prototype, "rollback", 1);
57919
58684
  __decorateClass([
57920
58685
  (0, import_common8.Get)("controllers/:id/configuration/revisions/:revision/preview"),
@@ -57922,7 +58687,7 @@ __decorateClass([
57922
58687
  __decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe))
57923
58688
  ], WagoControllerApi.prototype, "previewRevision", 1);
57924
58689
  WagoControllerApi = __decorateClass([
57925
- (0, import_plugins_backend_sdk12.Auth)("resources.update"),
58690
+ (0, import_plugins_backend_sdk13.Auth)("resources.update"),
57926
58691
  (0, import_common8.Controller)("wago"),
57927
58692
  __decorateParam(0, (0, import_common8.Inject)(WagoService)),
57928
58693
  __decorateParam(1, (0, import_common8.Inject)(WagoCommissioningService)),
@@ -57931,7 +58696,7 @@ WagoControllerApi = __decorateClass([
57931
58696
 
57932
58697
  // backend/wago-artifacts.controller.ts
57933
58698
  var import_common9 = require("@nestjs/common");
57934
- var import_plugins_backend_sdk13 = require("@attraccess/plugins-backend-sdk");
58699
+ var import_plugins_backend_sdk14 = require("@attraccess/plugins-backend-sdk");
57935
58700
  var import_platform_express = __toESM(require_platform_express());
57936
58701
  var import_promises5 = require("node:fs/promises");
57937
58702
  var import_node_path3 = require("node:path");
@@ -58149,7 +58914,7 @@ __decorateClass([
58149
58914
  __decorateParam(0, (0, import_common9.UploadedFiles)())
58150
58915
  ], WagoArtifactsController.prototype, "import", 1);
58151
58916
  WagoArtifactsController = __decorateClass([
58152
- (0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
58917
+ (0, import_plugins_backend_sdk14.Auth)("system.settings.manage"),
58153
58918
  (0, import_common9.Controller)("wago/runtime-artifacts"),
58154
58919
  __decorateParam(0, (0, import_common9.Inject)(WagoRuntimeArtifactsService))
58155
58920
  ], WagoArtifactsController);
@@ -58619,6 +59384,167 @@ WagoFlowService = __decorateClass([
58619
59384
  __decorateParam(0, (0, import_common10.Inject)(PLUGIN_CONTEXT4))
58620
59385
  ], WagoFlowService);
58621
59386
 
59387
+ // backend/diagnostics.controller.ts
59388
+ var import_common12 = require("@nestjs/common");
59389
+ var import_plugins_backend_sdk16 = require("@attraccess/plugins-backend-sdk");
59390
+
59391
+ // backend/diagnostics.service.ts
59392
+ var import_common11 = require("@nestjs/common");
59393
+ var import_plugins_backend_sdk15 = require("@attraccess/plugins-backend-sdk");
59394
+ function own(values, key) {
59395
+ return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
59396
+ }
59397
+ function metadataFromProvenance(provenance) {
59398
+ if (!provenance) return { names: {}, presets: [] };
59399
+ try {
59400
+ return editorMetadata(JSON.parse(provenance).editor);
59401
+ } catch {
59402
+ return { names: {}, presets: [] };
59403
+ }
59404
+ }
59405
+ function diagnosticReferences(nodes, channelIds, revision, capabilities) {
59406
+ return nodes.map((node) => {
59407
+ const channelId = typeof node.data.channelId === "string" ? node.data.channelId : "";
59408
+ const control = node.type === "plugin.wago.command";
59409
+ return {
59410
+ nodeId: node.id,
59411
+ resourceId: node.resourceId,
59412
+ channelId,
59413
+ control,
59414
+ href: `/resources/${node.resourceId}/flows`,
59415
+ invalid: !channelIds.includes(channelId) || control && (node.data.expectedConfigurationRevision !== revision || capabilities && (!own(capabilities, channelId)?.includes("output") || node.data.action === "pulse" && !own(capabilities, channelId)?.includes("pulse"))),
59416
+ conflict: control && nodes.some(
59417
+ (other) => other.type === "plugin.wago.command" && other.resourceId !== node.resourceId && other.data.channelId === channelId
59418
+ )
59419
+ };
59420
+ });
59421
+ }
59422
+ var WagoDiagnosticsService = class {
59423
+ constructor(context, wago) {
59424
+ __publicField(this, "context", context);
59425
+ __publicField(this, "wago", wago);
59426
+ }
59427
+ async get(controllerId) {
59428
+ const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
59429
+ if (!controller) throw new import_common11.NotFoundException("WAGO controller not found");
59430
+ const [draft, latest, applied, nodes] = await Promise.all([
59431
+ this.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId }),
59432
+ this.context.getRepository(WagoConfigurationRevision).findOne({ where: { controllerId }, order: { revision: "DESC" } }),
59433
+ this.context.getRepository(WagoConfigurationRevision).findOne({ where: { controllerId, state: "applied" }, order: { revision: "DESC" } }),
59434
+ this.context.dataSource.getRepository(import_plugins_backend_sdk15.ResourceFlowNode).createQueryBuilder("node").where("node.type LIKE :type", { type: "plugin.wago.%" }).andWhere("node.data ->> 'controllerId' = :controllerId", { controllerId }).take(1001).getMany()
59435
+ ]);
59436
+ const runtime = this.wago.diagnostics.read(controllerId);
59437
+ const snapshot = latest ? JSON.parse(latest.snapshot) : null;
59438
+ const appliedSnapshot = applied ? JSON.parse(applied.snapshot) : null;
59439
+ const heartbeatAt = runtime.heartbeatAt ?? controller.lastHeartbeatAt;
59440
+ const heartbeatFreshness = freshness(heartbeatAt);
59441
+ const revisionMismatch = !!latest && (applied?.revision !== latest.revision || runtime.revision !== latest.revision || runtime.activeStream !== void 0 && runtime.contentHash !== latest.contentHash);
59442
+ const connected = runtime.connected === false ? false : heartbeatFreshness === "fresh" || freshness(runtime.stateSourceAt) === "fresh";
59443
+ const references = diagnosticReferences(
59444
+ nodes.slice(0, 1e3),
59445
+ appliedSnapshot?.logicalChannels.map((channel) => channel.id) ?? [],
59446
+ applied?.revision ?? null,
59447
+ Object.fromEntries(appliedSnapshot?.logicalChannels.map((channel) => [channel.id, channel.capabilities]) ?? [])
59448
+ );
59449
+ const validationErrors = draft ? validateSnapshot(JSON.parse(draft.snapshot)) : [];
59450
+ return {
59451
+ controllerId,
59452
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
59453
+ name: controller.name ?? controller.hardwareId,
59454
+ connectivity: controller.trustState !== "claimed" ? "untrusted" : runtime.connected === false ? "disconnected" : connected ? "online" : "stale",
59455
+ heartbeatAt,
59456
+ heartbeatFreshness,
59457
+ runtimeVersion: controller.runtimeVersion,
59458
+ protocolVersion: controller.protocolVersion,
59459
+ capabilities: JSON.parse(controller.capabilities).slice(0, 64),
59460
+ incompatible: !!controller.compatibilityError,
59461
+ sequenceGaps: runtime.activeStream ? runtime.sequenceGaps : null,
59462
+ activeStream: runtime.activeStream ?? null,
59463
+ trackingExhausted: runtime.trackingExhausted,
59464
+ stateConnected: runtime.connected ?? null,
59465
+ stateHardwareAvailable: runtime.hardwareAvailable ?? null,
59466
+ stateSourceAt: runtime.stateSourceAt ?? null,
59467
+ sequenceExplanation: runtime.activeStream ? "Gaps are scoped to boot UUID and message category; duplicates and retired streams are ignored." : "Legacy payloads have no source envelope; sequence gaps and source freshness are unavailable.",
59468
+ configuration: {
59469
+ draftUpdatedAt: draft?.updatedAt ?? null,
59470
+ draftChanged: !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash(metadataFromProvenance(draft.presetProvenance)) !== configurationHash(metadataFromProvenance(latest.presetProvenance))),
59471
+ validationErrorCount: validationErrors.length,
59472
+ // Codes originate in our validator. Omit messages and dynamic paths, which can include arbitrary draft values.
59473
+ validationCodes: [...new Set(validationErrors.map((error) => error.code))].slice(0, 50),
59474
+ validationErrors: safeValidationSummaries(validationErrors),
59475
+ rejectionErrors: latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [],
59476
+ publishedRevision: latest?.revision ?? null,
59477
+ publishedState: latest?.state ?? null,
59478
+ appliedRevision: applied?.revision ?? null,
59479
+ reportedRevision: runtime.revision ?? null,
59480
+ revisionMismatch,
59481
+ rejected: latest?.state === "rejected"
59482
+ },
59483
+ hardwareReadiness: "unknown",
59484
+ hardwareReadinessReason: "Reported hardware availability is shown when supplied; it does not prove physical I/O readiness. Applied configuration and cached output state are not physical proof.",
59485
+ channels: (snapshot?.logicalChannels ?? []).slice(0, 256).map((channel) => {
59486
+ const values = [
59487
+ channel.capabilities.includes("input") ? own(runtime.inputs, channel.id) : void 0,
59488
+ channel.capabilities.includes("output") ? own(runtime.outputs, channel.id) : void 0,
59489
+ channel.capabilities.includes("measurement") ? own(runtime.measurements, channel.id) : void 0,
59490
+ channel.capabilities.includes("measurement") ? own(runtime.cumulativeMeasurements, channel.id) : void 0
59491
+ ].filter((value) => value !== void 0);
59492
+ const samples = values.map((value) => {
59493
+ const sourceFreshness = freshness(value.sourceAt);
59494
+ const availabilityReason = controller.trustState !== "claimed" ? "untrusted" : controller.compatibilityError ? "incompatible-runtime" : runtime.trackingExhausted ? "stream-tracking-exhausted" : runtime.connected !== true || !connected ? "disconnected-or-unknown" : runtime.hardwareAvailable === false ? "hardware-unavailable" : revisionMismatch || !applied ? "configuration-mismatch" : own(runtime.faults, channel.id) ? "recent-fault" : freshness(runtime.stateSourceAt) !== "fresh" ? "state-source-unavailable-or-stale" : value.streamId !== runtime.activeStream ? "old-or-legacy-stream" : sourceFreshness !== "fresh" ? `source-${sourceFreshness}` : "current";
59495
+ return { ...value, sourceFreshness, current: availabilityReason === "current", availabilityReason };
59496
+ });
59497
+ return {
59498
+ id: channel.id,
59499
+ profile: channel.profile,
59500
+ capabilities: channel.capabilities,
59501
+ disconnectPolicy: channel.disconnectPolicy,
59502
+ safeState: channel.capabilities.includes("output") ? "off (runtime default)" : "not applicable",
59503
+ samples,
59504
+ current: samples.length > 0 && samples.every((value) => value.current),
59505
+ fault: own(runtime.faults, channel.id) ?? null,
59506
+ acknowledgement: own(runtime.acknowledgements, channel.id) ?? null
59507
+ };
59508
+ }),
59509
+ faults: Object.entries(runtime.faults).map(([channelId, fault]) => ({ channelId, ...fault })),
59510
+ references,
59511
+ referencesTruncated: nodes.length > 1e3,
59512
+ events: runtime.events,
59513
+ limitations: [
59514
+ "Hardware readiness remains unknown. Legacy samples have no source time and are never current.",
59515
+ "Diagnostics retain at most 256 channels, 50 recent events and 15 minutes in this process.",
59516
+ "Resource-page slots are not integrated. Flow links open the affected resource flow; node IDs identify the exact nodes.",
59517
+ "The MQTT SDK exposes neither retained-message flags nor broker connection events. Source age is bounded to 90 seconds; immediate broker-disconnect detection is unavailable.",
59518
+ "Stream tracking keeps at most 16 retired boot UUIDs per controller and six category counters, and fails closed when exhausted. Replayed source times never become receipt times."
59519
+ ]
59520
+ };
59521
+ }
59522
+ };
59523
+ WagoDiagnosticsService = __decorateClass([
59524
+ (0, import_common11.Injectable)(),
59525
+ __decorateParam(0, (0, import_common11.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
59526
+ __decorateParam(1, (0, import_common11.Inject)(WagoService))
59527
+ ], WagoDiagnosticsService);
59528
+
59529
+ // backend/diagnostics.controller.ts
59530
+ var WagoDiagnosticsController = class {
59531
+ constructor(diagnostics) {
59532
+ __publicField(this, "diagnostics", diagnostics);
59533
+ }
59534
+ get(id) {
59535
+ return this.diagnostics.get(id);
59536
+ }
59537
+ };
59538
+ __decorateClass([
59539
+ (0, import_common12.Get)("controllers/:id/diagnostics"),
59540
+ __decorateParam(0, (0, import_common12.Param)("id", import_common12.ParseIntPipe))
59541
+ ], WagoDiagnosticsController.prototype, "get", 1);
59542
+ WagoDiagnosticsController = __decorateClass([
59543
+ (0, import_plugins_backend_sdk16.Auth)("resources.update"),
59544
+ (0, import_common12.Controller)("wago"),
59545
+ __decorateParam(0, (0, import_common12.Inject)(WagoDiagnosticsService))
59546
+ ], WagoDiagnosticsController);
59547
+
58622
59548
  // backend/plugin.ts
58623
59549
  var PLUGIN_CONTEXT5 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
58624
59550
  var WagoPluginModule = class {
@@ -58675,7 +59601,7 @@ var plugin = {
58675
59601
  flowService = new WagoFlowService(context);
58676
59602
  return {
58677
59603
  module: WagoPluginModule,
58678
- controllers: [WagoControllerApi, WagoArtifactsController],
59604
+ controllers: [WagoControllerApi, WagoArtifactsController, WagoDiagnosticsController],
58679
59605
  providers: [
58680
59606
  { provide: PLUGIN_CONTEXT5, useValue: context },
58681
59607
  WagoService,
@@ -58683,6 +59609,7 @@ var plugin = {
58683
59609
  WagoArtifactUploadInterceptor,
58684
59610
  WagoCommissioningReadiness,
58685
59611
  WagoCommissioningService,
59612
+ WagoDiagnosticsService,
58686
59613
  { provide: WagoFlowService, useValue: flowService }
58687
59614
  ]
58688
59615
  };