@attraccess/plugin-wago 0.1.0-nightly.1059.1 → 0.1.0-nightly.1064.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
@@ -5346,6 +5346,7 @@ var WagoService = class {
5346
5346
  if (canonical && (admitted || !canTrackDiagnostics) && typeof rawHeartbeat.streamId === "string" && typeof rawHeartbeat.timestamp === "string") {
5347
5347
  this.runtimeStatusHandler?.(controller.id, {
5348
5348
  imageId: heartbeat.runtimeImageId ?? "",
5349
+ runtimeVersion: heartbeat.runtimeVersion,
5349
5350
  streamId: rawHeartbeat.streamId,
5350
5351
  timestamp: Date.parse(rawHeartbeat.timestamp),
5351
5352
  receivedAt: Date.now(),
@@ -9345,15 +9346,40 @@ require_transaction
9345
9346
  case "$(cat "$tx/phase")" in
9346
9347
  accepted|accepted-cleaning)
9347
9348
  owned_new_container || fail 'Foreign runtime container'
9349
+ test "$(docker inspect --format '{{.Image}}' attraccess-wago)" = "$(cat "$tx/image-id")" || fail 'Accepted runtime image changed'
9348
9350
  phase accepted-cleaning
9349
9351
  previous=$(docker container ls -a --no-trunc --filter 'name=^/attraccess-wago.previous$' --format '{{.ID}}')
9350
9352
  if test -n "$previous"; then
9351
9353
  test "$previous" = "$(cat "$tx/previous-id")" || fail 'Foreign previous runtime'
9352
9354
  docker rm "$previous" >/dev/null
9353
- fi ;;
9354
- restored) ;;
9355
+ fi
9356
+ retired_image=$(cat "$tx/previous-image-id") ;;
9357
+ restored)
9358
+ test "$(docker inspect --format '{{.Image}}' attraccess-wago)" = "$(cat "$tx/previous-image-id")" || fail 'Restored runtime image changed'
9359
+ retired_image=$(cat "$tx/image-id") ;;
9355
9360
  *) fail 'Update has not been accepted or restored' ;;
9356
9361
  esac
9362
+ # Keep the receipt until image cleanup succeeds. A crash after image removal is
9363
+ # harmless: a successful inventory distinguishes an absent image from a failed
9364
+ # Docker query. Never force removal or prune images outside this transaction.
9365
+ retired_digest=\${retired_image#sha256:}
9366
+ test "$retired_image" != "$retired_digest" && test "\${#retired_digest}" = 64 || fail 'Invalid retired image identity'
9367
+ case "$retired_digest" in *[!a-f0-9]*) fail 'Invalid retired image identity' ;; esac
9368
+ images=$(docker image ls --no-trunc --quiet) || fail 'Cannot inventory runtime images'
9369
+ printf '%s\\n' "$images" | awk 'NF && ($0 !~ /^sha256:[a-f0-9]+$/ || length($0)!=71) {bad=1} END {exit bad}' || fail 'Invalid runtime image inventory'
9370
+ if printf '%s\\n' "$images" | grep -Fxq "$retired_image"; then
9371
+ containers=$(docker container ls -a --no-trunc --format '{{.ID}}') || fail 'Cannot inventory runtime containers'
9372
+ referenced=0
9373
+ for container in $containers; do
9374
+ container_image=$(docker inspect --format '{{.Image}}' "$container") || fail 'Cannot inspect runtime image references'
9375
+ if test "$container_image" = "$retired_image"; then referenced=1; fi
9376
+ done
9377
+ # Stopped containers also retain their images. Docker enforces this again if
9378
+ # an unrelated workload appears after the reference inventory.
9379
+ if test "$referenced" = 0; then
9380
+ docker image rm "$retired_image" >/dev/null || fail 'Retired runtime image cleanup failed'
9381
+ fi
9382
+ fi
9357
9383
  mv "$tx" "$cleanup"
9358
9384
  sync
9359
9385
  rm -rf "$cleanup"
@@ -10182,6 +10208,9 @@ var import_node_stream2 = require("node:stream");
10182
10208
 
10183
10209
  // backend/wago-runtime-update.ts
10184
10210
  var import_node_crypto14 = require("node:crypto");
10211
+ function runtimeTargetImageId(desired, installed) {
10212
+ return installed.imageId && installed.runtimeVersion === desired.manifest.runtimeVersion ? installed.imageId : desired.imageId;
10213
+ }
10185
10214
  var RuntimeUpdateError = class extends Error {
10186
10215
  constructor(failure, storageDiagnostics) {
10187
10216
  super(
@@ -10297,7 +10326,7 @@ var WagoRuntimeUpdateCoordinator = class {
10297
10326
  }
10298
10327
  const desired = await this.desired();
10299
10328
  assertOwned();
10300
- const contradictedCurrent = record2?.phase === "current" && observedImageId !== void 0 && observedImageId !== desired.imageId;
10329
+ const contradictedCurrent = record2?.phase === "current" && observedImageId !== void 0 && observedImageId !== (record2.currentImageId ?? record2.desiredImageId);
10301
10330
  if (record2 && !contradictedCurrent && record2.desiredImageId === desired.imageId && record2.retryAt > this.now())
10302
10331
  return "deferred";
10303
10332
  const attempt = (record2?.desiredImageId === desired.imageId ? record2.attempt : 0) + 1;
@@ -10368,11 +10397,12 @@ var WagoRuntimeUpdateCoordinator = class {
10368
10397
  return "settled";
10369
10398
  }
10370
10399
  }
10371
- if (inspection.imageId === desired.imageId) {
10400
+ const targetImageId = runtimeTargetImageId(desired, inspection);
10401
+ if (inspection.imageId === targetImageId) {
10372
10402
  const since = record2.startedAt;
10373
10403
  try {
10374
- const proof = await this.host.verify(controllerId, null, desired.imageId, since, operation.signal);
10375
- if (!proof.permanent || !proof.ready || proof.imageId !== desired.imageId || !Number.isSafeInteger(proof.observedAt) || proof.observedAt <= since || proof.observedAt > this.now())
10404
+ const proof = await this.host.verify(controllerId, null, targetImageId, since, operation.signal);
10405
+ if (!proof.permanent || !proof.ready || proof.imageId !== targetImageId || !Number.isSafeInteger(proof.observedAt) || proof.observedAt <= since || proof.observedAt > this.now())
10376
10406
  throw new RuntimeUpdateError("readiness");
10377
10407
  await this.assertCurrent(desired.imageId);
10378
10408
  } catch (error) {
@@ -10384,6 +10414,7 @@ var WagoRuntimeUpdateCoordinator = class {
10384
10414
  return "settled";
10385
10415
  }
10386
10416
  record2.phase = "current";
10417
+ record2.currentImageId = targetImageId;
10387
10418
  record2.attempt = 0;
10388
10419
  record2.retryAt = this.now() + CURRENT_RECHECK_MS;
10389
10420
  await persist(record2);
@@ -10417,6 +10448,7 @@ var WagoRuntimeUpdateCoordinator = class {
10417
10448
  await persist(record2);
10418
10449
  await this.host.accept(controllerId, record2.token, operation.signal);
10419
10450
  record2.phase = "current";
10451
+ record2.currentImageId = desired.imageId;
10420
10452
  record2.attempt = 0;
10421
10453
  record2.retryAt = this.now() + CURRENT_RECHECK_MS;
10422
10454
  await persist(record2);
@@ -10842,7 +10874,7 @@ var WagoManagedRuntimeService = class {
10842
10874
  __publicField(this, "coordinator");
10843
10875
  __publicField(this, "heartbeats", /* @__PURE__ */ new Map());
10844
10876
  __publicField(this, "heartbeatStreams", /* @__PURE__ */ new Map());
10845
- __publicField(this, "previousBoots", /* @__PURE__ */ new Map());
10877
+ __publicField(this, "runtimeActivations", /* @__PURE__ */ new Map());
10846
10878
  __publicField(this, "verifyingControllers", /* @__PURE__ */ new Set());
10847
10879
  __publicField(this, "connectingControllers", /* @__PURE__ */ new Set());
10848
10880
  __publicField(this, "reconciliationFailures", /* @__PURE__ */ new Map());
@@ -10888,7 +10920,7 @@ var WagoManagedRuntimeService = class {
10888
10920
  this.heartbeatStreams.delete(oldest);
10889
10921
  }
10890
10922
  }
10891
- if (!previous || previous.runtimePolicyToken !== heartbeat.runtimePolicyToken || previous.streamId !== heartbeat.streamId || previous.imageId !== heartbeat.imageId || heartbeat.receivedAt - previous.receivedAt > 9e4) {
10923
+ if (!previous || previous.runtimePolicyToken !== heartbeat.runtimePolicyToken || previous.streamId !== heartbeat.streamId || previous.imageId !== heartbeat.imageId || previous.runtimeVersion !== heartbeat.runtimeVersion || heartbeat.receivedAt - previous.receivedAt > 9e4) {
10892
10924
  this.wago.blockRuntime?.(id);
10893
10925
  if (!this.connectingControllers.has(id) && this.connectingControllers.size < 200) {
10894
10926
  this.connectingControllers.add(id);
@@ -11039,7 +11071,10 @@ var WagoManagedRuntimeService = class {
11039
11071
  runningVersion: controller.runtimeVersion,
11040
11072
  runningImageId: this.heartbeats.get(controller.id)?.imageId || null,
11041
11073
  desiredVersion: desired?.manifest.runtimeVersion ?? null,
11042
- desiredImageId: desired && "imageId" in desired ? desired.imageId : null
11074
+ desiredImageId: desired && "imageId" in desired ? runtimeTargetImageId(desired, {
11075
+ imageId: this.heartbeats.get(controller.id)?.imageId ?? "",
11076
+ runtimeVersion: this.heartbeats.get(controller.id)?.runtimeVersion ?? controller.runtimeVersion
11077
+ }) : null
11043
11078
  }
11044
11079
  } : {},
11045
11080
  ...access?.controllerId && this.wago.isRuntimeUpdateRequired ? { runtimeUpdateRequired: this.wago.isRuntimeUpdateRequired(access.controllerId) } : {},
@@ -11330,9 +11365,13 @@ var WagoManagedRuntimeService = class {
11330
11365
  if (!heartbeat || this.destroyed) return;
11331
11366
  const access = await this.access.findOne({ where: { controllerId: id }, order: { sessionId: "DESC" } });
11332
11367
  const enrolling = access?.state === "verified" || access?.state === "recovery_required";
11368
+ const controller = await this.controllers.findOneBy({ id, trustState: "claimed" });
11333
11369
  await this.wago.setRuntimePolicy?.(
11334
11370
  id,
11335
- enrolling ? heartbeat.imageId : desired.imageId,
11371
+ enrolling ? heartbeat.imageId : runtimeTargetImageId(desired, {
11372
+ imageId: heartbeat.imageId,
11373
+ runtimeVersion: heartbeat.runtimeVersion ?? controller?.runtimeVersion
11374
+ }),
11336
11375
  heartbeat.imageId,
11337
11376
  heartbeat.runtimePolicyToken
11338
11377
  );
@@ -11631,7 +11670,7 @@ var WagoManagedRuntimeService = class {
11631
11670
  if (!match) throw new RuntimeUpdateError("incompatible");
11632
11671
  return {
11633
11672
  imageId: match[2],
11634
- runtimeVersion: controller?.runtimeVersion,
11673
+ runtimeVersion: this.heartbeats.get(id)?.runtimeVersion ?? controller?.runtimeVersion,
11635
11674
  claimed: !!controller,
11636
11675
  managed: true,
11637
11676
  compatible: true,
@@ -11678,10 +11717,13 @@ var WagoManagedRuntimeService = class {
11678
11717
  await bundle.cleanup();
11679
11718
  }
11680
11719
  },
11681
- activate: async (id, token, _artifact, signal) => {
11720
+ activate: async (id, token, artifact, signal) => {
11682
11721
  const heartbeat = this.heartbeats.get(id);
11683
- if (!heartbeat || Date.now() - heartbeat.receivedAt > 9e4) throw new RuntimeUpdateError("offline");
11684
- this.previousBoots.set(token, heartbeat.streamId);
11722
+ this.runtimeActivations.set(token, {
11723
+ imageId: artifact.imageId,
11724
+ startedAt: Date.now(),
11725
+ previousStreamId: heartbeat?.streamId
11726
+ });
11685
11727
  this.verifyingControllers.add(id);
11686
11728
  await command(id, "activate", token, signal);
11687
11729
  },
@@ -11689,11 +11731,13 @@ var WagoManagedRuntimeService = class {
11689
11731
  const controller = await this.controllers.findOneByOrFail({ id, trustState: "claimed" });
11690
11732
  if (!controller.mqttServerId) throw new RuntimeUpdateError("offline");
11691
11733
  const prefix = (await this.wago.getSettings()).operationalPrefix;
11734
+ const activation = _token === null ? void 0 : this.runtimeActivations.get(_token);
11735
+ const freshSince = Math.max(since, activation?.startedAt ?? since);
11692
11736
  for (let attempt = 0; attempt < 120; attempt++) {
11693
11737
  signal.throwIfAborted();
11694
11738
  const heartbeat = this.heartbeats.get(id);
11695
11739
  const state = this.readiness.observe(controller.mqttServerId, controller.hardwareId, prefix);
11696
- if (heartbeat?.imageId === imageId && (_token === null || this.previousBoots.has(_token) && heartbeat.streamId !== this.previousBoots.get(_token)) && heartbeat.timestamp > since && heartbeat.receivedAt > since && Date.now() - heartbeat.receivedAt < 9e4 && state?.timestamp > since && Date.now() - state.timestamp < 9e4 && state.streamId === heartbeat.streamId && state.ready)
11740
+ if (heartbeat?.imageId === imageId && (_token === null || activation?.imageId === imageId && heartbeat.streamId !== activation.previousStreamId) && heartbeat.timestamp > freshSince && heartbeat.receivedAt > freshSince && Date.now() - heartbeat.receivedAt < 9e4 && state?.timestamp > freshSince && Date.now() - state.timestamp < 9e4 && state.streamId === heartbeat.streamId && state.ready)
11697
11741
  return { imageId, observedAt: heartbeat.receivedAt, permanent: true, ready: true };
11698
11742
  await new Promise((resolve2) => setTimeout(resolve2, 1e3));
11699
11743
  }
@@ -11706,7 +11750,7 @@ var WagoManagedRuntimeService = class {
11706
11750
  try {
11707
11751
  await command(id, "acknowledge", token, signal);
11708
11752
  } finally {
11709
- this.previousBoots.delete(token);
11753
+ this.runtimeActivations.delete(token);
11710
11754
  this.verifyingControllers.delete(id);
11711
11755
  }
11712
11756
  },
@@ -11714,7 +11758,7 @@ var WagoManagedRuntimeService = class {
11714
11758
  try {
11715
11759
  await command(id, "recover", token, signal, previous);
11716
11760
  } finally {
11717
- this.previousBoots.delete(token);
11761
+ this.runtimeActivations.delete(token);
11718
11762
  this.verifyingControllers.delete(id);
11719
11763
  }
11720
11764
  }
@@ -11750,7 +11794,7 @@ var WagoManagedRuntimeService = class {
11750
11794
  await stopped;
11751
11795
  this.heartbeats.clear();
11752
11796
  this.heartbeatStreams.clear();
11753
- this.previousBoots.clear();
11797
+ this.runtimeActivations.clear();
11754
11798
  this.verifyingControllers.clear();
11755
11799
  this.enrolmentProgress.clear();
11756
11800
  }
@@ -15777,6 +15821,81 @@ WagoUpdatesController = __decorateClass([
15777
15821
  __decorateParam(1, (0, import_common17.Inject)(WagoNetworkChangeService))
15778
15822
  ], WagoUpdatesController);
15779
15823
 
15824
+ // backend/wago-live-updates.service.ts
15825
+ var import_common18 = require("@nestjs/common");
15826
+ var import_plugins_backend_sdk21 = require("@attraccess/plugins-backend-sdk");
15827
+ var WagoLiveUpdatesService = class {
15828
+ constructor(context, wago, commissioning, diagnostics, managed, network) {
15829
+ __publicField(this, "context", context);
15830
+ __publicField(this, "wago", wago);
15831
+ __publicField(this, "commissioning", commissioning);
15832
+ __publicField(this, "diagnostics", diagnostics);
15833
+ __publicField(this, "managed", managed);
15834
+ __publicField(this, "network", network);
15835
+ __publicField(this, "sample", (0, import_plugins_backend_sdk21.createSharedLiveSampler)());
15836
+ }
15837
+ onModuleInit() {
15838
+ const live = this.context.liveUpdates;
15839
+ if (!live) throw new Error("WAGO live updates require a host with plugin live-update support");
15840
+ const register = (topic, interval, permission, kind, read) => {
15841
+ live.register({
15842
+ topic,
15843
+ identifier: kind === "none" ? "none" : "required",
15844
+ authorize: async ({ identifier: identifier3 }, user) => {
15845
+ if (!user.effectivePermissions?.has(permission)) throw new import_common18.ForbiddenException();
15846
+ if (kind === "none") return;
15847
+ const pattern = kind === "revisions" ? /^[1-9]\d*:(0|[1-9]\d*)$/ : /^[1-9]\d*$/;
15848
+ if (!pattern.test(identifier3 ?? "")) throw new import_common18.NotFoundException();
15849
+ const [id, offset = 0] = identifier3.split(":").map(Number);
15850
+ if (!Number.isSafeInteger(id) || !Number.isSafeInteger(offset)) throw new import_common18.NotFoundException();
15851
+ const exists = kind === "session" ? await this.context.getRepository(WagoCommissioningSession).existsBy({ id }) : await this.context.getRepository(WagoController).existsBy({ id });
15852
+ if (!exists) throw new import_common18.NotFoundException();
15853
+ },
15854
+ source: ({ identifier: identifier3 }) => this.sample(`${topic}:${identifier3 ?? ""}`, interval, () => {
15855
+ const [id = 0, offset = 0] = (identifier3 ?? "").split(":").map(Number);
15856
+ return read(id, offset);
15857
+ })
15858
+ });
15859
+ };
15860
+ register("controllers", 1e4, "resources.update", "none", () => this.wago.list());
15861
+ register("commissioning-sessions", 2e3, "system.settings.manage", "none", () => this.commissioning.list(100, 0));
15862
+ register("diagnostics", 2e3, "resources.update", "controller", (id) => this.diagnostics.get(id));
15863
+ register(
15864
+ "configuration-baseline",
15865
+ 2e3,
15866
+ "resources.update",
15867
+ "controller",
15868
+ (id) => this.wago.getConfigurationBaseline(id)
15869
+ );
15870
+ register(
15871
+ "configuration-revisions",
15872
+ 2e3,
15873
+ "resources.update",
15874
+ "revisions",
15875
+ (id, offset) => this.wago.revisionsFor(id, offset)
15876
+ );
15877
+ register(
15878
+ "commissioning-verification",
15879
+ 5e3,
15880
+ "system.settings.manage",
15881
+ "session",
15882
+ (id) => this.commissioning.verification(id)
15883
+ );
15884
+ register("runtime-update", 5e3, "system.settings.manage", "controller", (id) => this.managed.status(id));
15885
+ register("managed-access", 5e3, "system.settings.manage", "session", (id) => this.managed.sessionStatus(id));
15886
+ register("network-change", 2e3, "system.settings.manage", "controller", (id) => this.network.status(id));
15887
+ }
15888
+ };
15889
+ WagoLiveUpdatesService = __decorateClass([
15890
+ (0, import_common18.Injectable)(),
15891
+ __decorateParam(0, (0, import_common18.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
15892
+ __decorateParam(1, (0, import_common18.Inject)(WagoService)),
15893
+ __decorateParam(2, (0, import_common18.Inject)(WagoCommissioningService)),
15894
+ __decorateParam(3, (0, import_common18.Inject)(WagoDiagnosticsService)),
15895
+ __decorateParam(4, (0, import_common18.Inject)(WagoManagedRuntimeService)),
15896
+ __decorateParam(5, (0, import_common18.Inject)(WagoNetworkChangeService))
15897
+ ], WagoLiveUpdatesService);
15898
+
15780
15899
  // backend/plugin.ts
15781
15900
  var PLUGIN_CONTEXT6 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
15782
15901
  var WagoPluginModule = class {
@@ -15835,7 +15954,8 @@ var plugin = {
15835
15954
  WagoManagedRuntimeService,
15836
15955
  WagoCommissioningService,
15837
15956
  WagoCredentialRotationService,
15838
- WagoNetworkChangeService
15957
+ WagoNetworkChangeService,
15958
+ WagoLiveUpdatesService
15839
15959
  ]
15840
15960
  };
15841
15961
  }
@@ -61,9 +61,6 @@ function createPluginApiClient(basePath = "") {
61
61
  };
62
62
  }
63
63
  //#endregion
64
- //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.slots.ts
65
- var RESOURCE_OVERVIEW_SLOT = "resource.overview";
66
- //#endregion
67
64
  //#region ../../../node_modules/.pnpm/react@19.2.8/node_modules/react/cjs/react-jsx-runtime.production.js
68
65
  /**
69
66
  * @license React
@@ -99,10 +96,64 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
99
96
  exports.jsxs = jsxProd;
100
97
  }));
101
98
  //#endregion
102
- //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.standard-drawer.tsx
99
+ //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.live-updates.tsx
103
100
  var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
104
101
  module.exports = require_react_jsx_runtime_production();
105
102
  })))();
103
+ var { createContext, useCallback, useContext, useEffect: useEffect$12, useRef: useRef$5 } = await importShared("react");
104
+ var contextKey = Symbol.for("attraccess.plugin.live-updates.context");
105
+ var contexts = globalThis;
106
+ var LiveContext = contexts[contextKey] ??= createContext(null);
107
+ function usePluginLiveUpdates(props) {
108
+ const client = useContext(LiveContext);
109
+ const latest = useRef$5({
110
+ props,
111
+ client
112
+ });
113
+ latest.current = {
114
+ props,
115
+ client
116
+ };
117
+ const unsubscribe = useRef$5(null);
118
+ const { plugin, topic, identifier, enabled = true } = props;
119
+ useEffect$12(() => {
120
+ if (!client || !enabled) return;
121
+ let active = true;
122
+ const matches = () => active && latest.current.client === client && latest.current.props.enabled !== false && latest.current.props.plugin === plugin && latest.current.props.topic === topic && latest.current.props.identifier === identifier;
123
+ const remove = client.subscribe({
124
+ topic: `plugin:${encodeURIComponent(plugin)}:${topic}`,
125
+ ...identifier !== void 0 ? { identifier } : {}
126
+ }, (payload) => {
127
+ if (matches()) return latest.current.props.onUpdate(payload);
128
+ }, () => {
129
+ if (matches()) return latest.current.props.onReconnect?.();
130
+ }, () => {
131
+ if (matches()) return latest.current.props.onUnavailable?.();
132
+ });
133
+ const cleanup = () => {
134
+ if (!active) return;
135
+ active = false;
136
+ remove();
137
+ };
138
+ unsubscribe.current = cleanup;
139
+ return () => {
140
+ cleanup();
141
+ if (unsubscribe.current === cleanup) unsubscribe.current = null;
142
+ };
143
+ }, [
144
+ client,
145
+ plugin,
146
+ topic,
147
+ identifier,
148
+ enabled
149
+ ]);
150
+ return { abort: useCallback(() => unsubscribe.current?.(), []) };
151
+ }
152
+ //#endregion
153
+ //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.slots.ts
154
+ var RESOURCE_OVERVIEW_SLOT = "resource.overview";
155
+ //#endregion
156
+ //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.standard-drawer.tsx
106
157
  var { DrawerBackdrop: DrawerBackdrop$1, DrawerContent: DrawerContent$1, DrawerDialog: DrawerDialog$1 } = await importShared("@heroui/react");
107
158
  //#endregion
108
159
  //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.standard-modal.tsx
@@ -112,6 +163,69 @@ var { Modal: Modal$3, ModalBackdrop: ModalBackdrop$2, ModalContainer: ModalConta
112
163
  /** Invalidate this prefix after plugin configuration changes to refresh canvas summaries. */
113
164
  var FLOW_NODE_PREVIEW_QUERY_KEY = ["flow-node-preview"];
114
165
  //#endregion
166
+ //#region frontend/src/live-updates.ts
167
+ var { useQueryClient: useQueryClient$4 } = await importShared("@tanstack/react-query");
168
+ var liveStates = /* @__PURE__ */ new WeakMap();
169
+ /** Initial reads/mutations remain REST; the host stream owns snapshots and reconnect refresh. */
170
+ function useWagoLiveQuery(queryKey, topic, identifier, enabled = true) {
171
+ const client = useQueryClient$4();
172
+ const onUpdate = (event) => {
173
+ const existingQuery = client.getQueryCache().find({
174
+ queryKey,
175
+ exact: true
176
+ });
177
+ const previous = existingQuery && liveStates.get(existingQuery);
178
+ const cancellation = client.cancelQueries({
179
+ queryKey,
180
+ exact: true
181
+ });
182
+ if (event.eventType === "snapshot") client.setQueryData(queryKey, event.value);
183
+ else {
184
+ const query = client.getQueryCache().find({
185
+ queryKey,
186
+ exact: true
187
+ });
188
+ query?.setState({
189
+ ...existingQuery === query && previous?.pendingRead === query.promise ? previous?.state : void 0,
190
+ status: "error",
191
+ error: /* @__PURE__ */ new Error("Live updates are temporarily unavailable."),
192
+ errorUpdatedAt: Date.now()
193
+ });
194
+ }
195
+ const query = client.getQueryCache().find({
196
+ queryKey,
197
+ exact: true
198
+ });
199
+ if (!query) return;
200
+ const state = query.state;
201
+ const pendingRead = query.promise;
202
+ liveStates.set(query, {
203
+ state,
204
+ pendingRead
205
+ });
206
+ return cancellation.then(() => {
207
+ if (liveStates.get(query)?.state === state && query.promise === pendingRead && client.getQueryCache().find({
208
+ queryKey,
209
+ exact: true
210
+ }) === query) {
211
+ liveStates.delete(query);
212
+ if (query.state !== state || query.state.fetchStatus !== "idle") query.setState({
213
+ ...state,
214
+ fetchStatus: "idle"
215
+ });
216
+ }
217
+ });
218
+ };
219
+ return usePluginLiveUpdates({
220
+ plugin: "wago",
221
+ topic,
222
+ identifier,
223
+ enabled,
224
+ onUpdate,
225
+ onUnavailable: () => onUpdate({ eventType: "unavailable" })
226
+ });
227
+ }
228
+ //#endregion
115
229
  //#region frontend/src/api.ts
116
230
  var api$5 = createPluginApiClient("/api/wago");
117
231
  var hostApi = createPluginApiClient("/api");
@@ -234,18 +348,17 @@ var queryKeys = {
234
348
  commissioningSessions: ["wago", "commissioning-sessions"]
235
349
  };
236
350
  function useControllersQuery() {
351
+ useWagoLiveQuery(queryKeys.controllers, "controllers");
237
352
  return useQuery$6({
238
353
  queryKey: queryKeys.controllers,
239
- queryFn: listControllers,
240
- refetchInterval: 1e4
354
+ queryFn: listControllers
241
355
  });
242
356
  }
243
357
  function useCommissioningSessionsQuery() {
358
+ useWagoLiveQuery(queryKeys.commissioningSessions, "commissioning-sessions");
244
359
  return useQuery$6({
245
360
  queryKey: queryKeys.commissioningSessions,
246
- queryFn: ({ signal }) => listCommissioningSessions(100, 0, signal),
247
- refetchInterval: 2e3,
248
- refetchIntervalInBackground: true
361
+ queryFn: ({ signal }) => listCommissioningSessions(100, 0, signal)
249
362
  });
250
363
  }
251
364
  function useSettingsQuery() {
@@ -350,10 +463,10 @@ function useDraftQuery(controllerId) {
350
463
  });
351
464
  }
352
465
  function useConfigurationRevisionsQuery(controllerId, offset) {
466
+ useWagoLiveQuery([...queryKeys.revisions(controllerId), offset], "configuration-revisions", `${controllerId}:${offset}`);
353
467
  return useQuery$6({
354
468
  queryKey: [...queryKeys.revisions(controllerId), offset],
355
- queryFn: () => listConfigurationRevisions(controllerId, offset),
356
- refetchInterval: 2e3
469
+ queryFn: () => listConfigurationRevisions(controllerId, offset)
357
470
  });
358
471
  }
359
472
  function useConfigurationRevisionPreviewQuery(controllerId, revision, enabled) {
@@ -3382,16 +3495,23 @@ function ClaimControllerModal({ controllerId, onOpenChange }) {
3382
3495
  //#region frontend/src/useRuntimeStatus.ts
3383
3496
  var { useQuery: useQuery$5 } = await importShared("@tanstack/react-query");
3384
3497
  function useRuntimeStatus(target) {
3498
+ const controllerId = target && "controller" in target ? target.controller.id : null;
3499
+ const sessionId = target && "session" in target ? target.session.id : null;
3500
+ useWagoLiveQuery([
3501
+ "wago",
3502
+ "runtime-update",
3503
+ controllerId,
3504
+ sessionId
3505
+ ], controllerId !== null ? "runtime-update" : "managed-access", String(controllerId ?? sessionId), !!target);
3385
3506
  return useQuery$5({
3386
3507
  queryKey: [
3387
3508
  "wago",
3388
3509
  "runtime-update",
3389
- target && "controller" in target ? target.controller.id : null,
3390
- target && "session" in target ? target.session.id : null
3510
+ controllerId,
3511
+ sessionId
3391
3512
  ],
3392
3513
  queryFn: () => target && "controller" in target ? getRuntimeUpdateStatus(target.controller.id) : target && "session" in target ? getManagedAccessStatus(target.session.id) : Promise.reject(/* @__PURE__ */ new Error("No controller selected")),
3393
3514
  enabled: !!target,
3394
- refetchInterval: 5e3,
3395
3515
  staleTime: 4e3,
3396
3516
  retry: false
3397
3517
  });
@@ -3692,6 +3812,11 @@ var { useEffect: useEffect$10, useState: useState$17 } = await importShared("rea
3692
3812
  /** Enrollment evidence is independent of configuration and physical qualification. */
3693
3813
  function useCommissioningVerification(session) {
3694
3814
  const enabled = ["awaiting_verification", "completed"].includes(session.state);
3815
+ useWagoLiveQuery([
3816
+ "wago",
3817
+ "commissioning-verification",
3818
+ session.id
3819
+ ], "commissioning-verification", String(session.id), enabled);
3695
3820
  const query = useQuery$4({
3696
3821
  queryKey: [
3697
3822
  "wago",
@@ -3699,9 +3824,7 @@ function useCommissioningVerification(session) {
3699
3824
  session.id
3700
3825
  ],
3701
3826
  queryFn: () => getCommissioningVerification(session.id),
3702
- enabled,
3703
- refetchInterval: 5e3,
3704
- refetchIntervalInBackground: true
3827
+ enabled
3705
3828
  });
3706
3829
  const [now, setNow] = useState$17(Date.now);
3707
3830
  useEffect$10(() => {
@@ -3740,10 +3863,10 @@ function NetworkChangeForm({ controllerId }) {
3740
3863
  "network-change",
3741
3864
  controllerId
3742
3865
  ];
3866
+ useWagoLiveQuery(queryKey, "network-change", String(controllerId));
3743
3867
  const statusQuery = useQuery$3({
3744
3868
  queryKey,
3745
3869
  queryFn: () => getNetworkChangeStatus(controllerId),
3746
- refetchInterval: 2e3,
3747
3870
  retry: false
3748
3871
  });
3749
3872
  const servers = useQuery$3({
@@ -9258,7 +9381,12 @@ function DeviceSettings({ configuration, device: original, onChange, onClose, is
9258
9381
  //#region frontend/src/diagnostics.ts
9259
9382
  var { useQuery: useQuery$2 } = await importShared("@tanstack/react-query");
9260
9383
  var api$1 = createPluginApiClient("/api/wago");
9261
- function useWagoDiagnostics(controllerId, refetchInterval = 5e3) {
9384
+ function useWagoDiagnostics(controllerId) {
9385
+ useWagoLiveQuery([
9386
+ "wago",
9387
+ "diagnostics",
9388
+ controllerId
9389
+ ], "diagnostics", String(controllerId), controllerId !== null);
9262
9390
  return useQuery$2({
9263
9391
  queryKey: [
9264
9392
  "wago",
@@ -9267,7 +9395,6 @@ function useWagoDiagnostics(controllerId, refetchInterval = 5e3) {
9267
9395
  ],
9268
9396
  queryFn: () => api$1.request(`/controllers/${controllerId}/diagnostics`),
9269
9397
  enabled: controllerId !== null,
9270
- refetchInterval,
9271
9398
  retry: 1
9272
9399
  });
9273
9400
  }
@@ -9300,16 +9427,20 @@ function useFrontPanel(controllerId) {
9300
9427
  ];
9301
9428
  const [restored] = useState$4(() => client.getQueryData(key));
9302
9429
  const draft = useDraftQuery(controllerId);
9430
+ useWagoLiveQuery([
9431
+ "wago",
9432
+ "configuration-baseline",
9433
+ controllerId
9434
+ ], "configuration-baseline", String(controllerId));
9303
9435
  const baseline = useQuery$1({
9304
9436
  queryKey: [
9305
9437
  "wago",
9306
9438
  "configuration-baseline",
9307
9439
  controllerId
9308
9440
  ],
9309
- queryFn: () => getConfigurationBaseline(controllerId),
9310
- refetchInterval: 2e3
9441
+ queryFn: () => getConfigurationBaseline(controllerId)
9311
9442
  });
9312
- const diagnostics = useWagoDiagnostics(controllerId, 2e3);
9443
+ const diagnostics = useWagoDiagnostics(controllerId);
9313
9444
  const [configuration, setConfiguration] = useState$4(restored?.configuration ?? null);
9314
9445
  const loadedDraft = useRef(restored?.loadedDraft ?? null);
9315
9446
  const [editing, setEditing] = useState$4(Boolean(restored));
@@ -75,7 +75,7 @@ var exportSet = /* @__PURE__ */ new Set([
75
75
  ]);
76
76
  var moduleMap = { "./plugin": () => {
77
77
  dynamicLoadingCss(["style.css"], false, "./plugin");
78
- return __federation_import("./__federation_expose_Plugin-BQawMHeJ.js").then((module) => Object.keys(module).every((item) => exportSet.has(item)) ? () => module.default : () => module);
78
+ return __federation_import("./__federation_expose_Plugin-KY-OYpkJ.js").then((module) => Object.keys(module).every((item) => exportSet.has(item)) ? () => module.default : () => module);
79
79
  } };
80
80
  var seen = {};
81
81
  var dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attraccess/plugin-wago",
3
- "version": "0.1.0-nightly.1059.1",
3
+ "version": "0.1.0-nightly.1064.1",
4
4
  "description": "Official Attraccess integration for WAGO CC100 controllers.",
5
5
  "keywords": [
6
6
  "attraccess-plugin",