@attraccess/plugin-wago 0.1.0-alpha.0 → 0.1.0-nightly.1002.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.
@@ -65,24 +65,24 @@ function createPluginApiClient(basePath = "") {
65
65
  var RESOURCE_OVERVIEW_SLOT = "resource.overview";
66
66
  //#endregion
67
67
  //#region frontend/src/api.ts
68
- var api$6 = createPluginApiClient("/api/wago");
68
+ var api$5 = createPluginApiClient("/api/wago");
69
69
  var hostApi = createPluginApiClient("/api");
70
- var listControllers = () => api$6.request("/controllers");
71
- var getSettings = () => api$6.request("/settings");
72
- var setSettings = (defaultMqttServerId) => api$6.request("/settings", {
70
+ var listControllers = () => api$5.request("/controllers");
71
+ var getSettings = () => api$5.request("/settings");
72
+ var setSettings = (defaultMqttServerId) => api$5.request("/settings", {
73
73
  method: "POST",
74
74
  body: { defaultMqttServerId }
75
75
  });
76
76
  var listMqttServers = () => hostApi.request("/mqtt/servers");
77
- var claimController = (id, input) => api$6.request(`/controllers/${id}/claim`, {
77
+ var claimController = (id, input) => api$5.request(`/controllers/${id}/claim`, {
78
78
  method: "POST",
79
79
  body: input
80
80
  });
81
- var createCommissioningSession = (input) => api$6.request("/commissioning/sessions", {
81
+ var createCommissioningSession = (input) => api$5.request("/commissioning/sessions", {
82
82
  method: "POST",
83
83
  body: input
84
84
  });
85
- var confirmCommissioningHostKey = (id, hostKeyFingerprint, physicalIdentityConfirmed = false) => api$6.request(`/commissioning/sessions/${id}/confirm-host-key`, {
85
+ var confirmCommissioningHostKey = (id, hostKeyFingerprint, physicalIdentityConfirmed = false) => api$5.request(`/commissioning/sessions/${id}/confirm-host-key`, {
86
86
  method: "POST",
87
87
  body: {
88
88
  hostKeyFingerprint,
@@ -90,22 +90,22 @@ var confirmCommissioningHostKey = (id, hostKeyFingerprint, physicalIdentityConfi
90
90
  trustMethod: physicalIdentityConfirmed ? "isolated_service_connection" : "trusted_inventory"
91
91
  }
92
92
  });
93
- var listCommissioningSessions = (limit = 100, offset = 0) => api$6.request(`/commissioning/sessions?limit=${limit}&offset=${offset}`);
94
- var getCommissioningVerification = (id) => api$6.request(`/commissioning/sessions/${id}/verification`);
95
- var getCommissioningSupport = () => api$6.request("/commissioning/support");
96
- var deliverCommissioningSession = (id, input) => api$6.request(`/commissioning/sessions/${id}/deliver`, {
93
+ var listCommissioningSessions = (limit = 100, offset = 0) => api$5.request(`/commissioning/sessions?limit=${limit}&offset=${offset}`);
94
+ var getCommissioningVerification = (id) => api$5.request(`/commissioning/sessions/${id}/verification`);
95
+ var getCommissioningSupport = () => api$5.request("/commissioning/support");
96
+ var deliverCommissioningSession = (id, input) => api$5.request(`/commissioning/sessions/${id}/deliver`, {
97
97
  method: "POST",
98
98
  body: input
99
99
  });
100
- var recoverCommissioningSession = (id, input) => api$6.request(`/commissioning/sessions/${id}/recover`, {
100
+ var recoverCommissioningSession = (id, input) => api$5.request(`/commissioning/sessions/${id}/recover`, {
101
101
  method: "POST",
102
102
  body: input
103
103
  });
104
- var removeCommissioningSession = (id) => api$6.request(`/commissioning/sessions/${id}`, { method: "DELETE" });
105
- var removeController = (id) => api$6.request(`/controllers/${id}`, { method: "DELETE" });
106
- var getDraft = (id) => api$6.request(`/controllers/${id}/configuration/draft`);
107
- var getConfigurationBaseline = (id) => api$6.request(`/controllers/${id}/configuration/baseline`);
108
- var saveDraft = (id, snapshot, metadata, expectedDraft) => api$6.request(`/controllers/${id}/configuration/draft`, {
104
+ var removeCommissioningSession = (id) => api$5.request(`/commissioning/sessions/${id}`, { method: "DELETE" });
105
+ var removeController = (id) => api$5.request(`/controllers/${id}`, { method: "DELETE" });
106
+ var getDraft = (id) => api$5.request(`/controllers/${id}/configuration/draft`);
107
+ var getConfigurationBaseline = (id) => api$5.request(`/controllers/${id}/configuration/baseline`);
108
+ var saveDraft = (id, snapshot, metadata, expectedDraft) => api$5.request(`/controllers/${id}/configuration/draft`, {
109
109
  method: "POST",
110
110
  body: {
111
111
  snapshot,
@@ -113,15 +113,15 @@ var saveDraft = (id, snapshot, metadata, expectedDraft) => api$6.request(`/contr
113
113
  expectedDraft
114
114
  }
115
115
  });
116
- var listPresets = () => api$6.request("/configuration/presets");
117
- var previewPreset = (id, application, snapshot) => api$6.request(`/controllers/${id}/configuration/presets/preview`, {
116
+ var listPresets = () => api$5.request("/configuration/presets");
117
+ var previewPreset = (id, application, snapshot) => api$5.request(`/controllers/${id}/configuration/presets/preview`, {
118
118
  method: "POST",
119
119
  body: {
120
120
  application,
121
121
  snapshot
122
122
  }
123
123
  });
124
- var applyPreset = (id, application, selectedPaths, previewedDraftHash, snapshot) => api$6.request(`/controllers/${id}/configuration/presets/apply`, {
124
+ var applyPreset = (id, application, selectedPaths, previewedDraftHash, snapshot) => api$5.request(`/controllers/${id}/configuration/presets/apply`, {
125
125
  method: "POST",
126
126
  body: {
127
127
  application,
@@ -130,28 +130,28 @@ var applyPreset = (id, application, selectedPaths, previewedDraftHash, snapshot)
130
130
  snapshot
131
131
  }
132
132
  });
133
- var validateConfiguration = (id, snapshot) => api$6.request(`/controllers/${id}/configuration/validate`, {
133
+ var validateConfiguration = (id, snapshot) => api$5.request(`/controllers/${id}/configuration/validate`, {
134
134
  method: "POST",
135
135
  body: { snapshot }
136
136
  });
137
- var reviewConfiguration = (id) => api$6.request(`/controllers/${id}/configuration/review`, { method: "POST" });
138
- var publishConfiguration = (id, force, reviewedHash) => api$6.request(`/controllers/${id}/configuration/publish`, {
137
+ var reviewConfiguration = (id) => api$5.request(`/controllers/${id}/configuration/review`, { method: "POST" });
138
+ var publishConfiguration = (id, force, reviewedHash) => api$5.request(`/controllers/${id}/configuration/publish`, {
139
139
  method: "POST",
140
140
  body: {
141
141
  force,
142
142
  reviewedHash
143
143
  }
144
144
  });
145
- var listConfigurationRevisions = (id, offset) => api$6.request(`/controllers/${id}/configuration/revisions?offset=${offset}&limit=20`);
146
- var previewConfigurationRevision = (id, revision) => api$6.request(`/controllers/${id}/configuration/revisions/${revision}/preview`);
147
- var acknowledgeConfigurationRejection = (id, revision, contentHash, reportedAt) => api$6.request(`/controllers/${id}/configuration/revisions/${revision}/acknowledge-rejection`, {
145
+ var listConfigurationRevisions = (id, offset) => api$5.request(`/controllers/${id}/configuration/revisions?offset=${offset}&limit=20`);
146
+ var previewConfigurationRevision = (id, revision) => api$5.request(`/controllers/${id}/configuration/revisions/${revision}/preview`);
147
+ var acknowledgeConfigurationRejection = (id, revision, contentHash, reportedAt) => api$5.request(`/controllers/${id}/configuration/revisions/${revision}/acknowledge-rejection`, {
148
148
  method: "POST",
149
149
  body: {
150
150
  contentHash,
151
151
  reportedAt
152
152
  }
153
153
  });
154
- var rollbackConfiguration = (id, revision, force, sourceHash, currentHash, draftHash) => api$6.request(`/controllers/${id}/configuration/rollback/${revision}`, {
154
+ var rollbackConfiguration = (id, revision, force, sourceHash, currentHash, draftHash) => api$5.request(`/controllers/${id}/configuration/rollback/${revision}`, {
155
155
  method: "POST",
156
156
  body: {
157
157
  force,
@@ -369,7 +369,7 @@ function useConfigurationActions(controllerId) {
369
369
  //#endregion
370
370
  //#region frontend/src/ClaimControllerModal.tsx
371
371
  var import_jsx_runtime = require_jsx_runtime();
372
- var { Alert: Alert$9, Button: Button$18, Form: Form$3, Input: Input$7, Label: Label$10, Modal: Modal$2, ModalBackdrop: ModalBackdrop$1, ModalBody: ModalBody$1, ModalContainer: ModalContainer$1, ModalDialog: ModalDialog$1, ModalFooter: ModalFooter$1, ModalHeader: ModalHeader$1, ModalHeading: ModalHeading$1, TextField: TextField$7 } = await importShared("@heroui/react");
372
+ var { Alert: Alert$8, Button: Button$17, Form: Form$3, Input: Input$6, Label: Label$9, Modal: Modal$2, ModalBackdrop: ModalBackdrop$1, ModalBody: ModalBody$1, ModalContainer: ModalContainer$1, ModalDialog: ModalDialog$1, ModalFooter: ModalFooter$1, ModalHeader: ModalHeader$1, ModalHeading: ModalHeading$1, TextField: TextField$6 } = await importShared("@heroui/react");
373
373
  var { useEffectEvent, useState: useState$15 } = await importShared("react");
374
374
  function ClaimControllerModal({ controllerId, onOpenChange }) {
375
375
  const claimMutation = useClaimControllerMutation();
@@ -412,31 +412,31 @@ function ClaimControllerModal({ controllerId, onOpenChange }) {
412
412
  className: "wg:text-sm wg:text-muted",
413
413
  children: "Verify physical access to this controller before it can receive commands."
414
414
  }),
415
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$7, {
415
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$6, {
416
416
  isRequired: true,
417
417
  name: "name",
418
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$10, { children: "Controller name" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$7, {
418
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$9, { children: "Controller name" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$6, {
419
419
  value: name,
420
420
  onChange: (event) => setName(event.target.value)
421
421
  })]
422
422
  }),
423
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$7, {
423
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$6, {
424
424
  isRequired: true,
425
425
  name: "verifier",
426
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$10, { children: "Physical pairing code or fingerprint" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$7, {
426
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$9, { children: "Physical pairing code or fingerprint" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$6, {
427
427
  value: verifier,
428
428
  onChange: (event) => setVerifier(event.target.value)
429
429
  })]
430
430
  }),
431
- claimMutation.isError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$9, {
431
+ claimMutation.isError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$8, {
432
432
  status: "danger",
433
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$9.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$9.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$9.Description, { children: getErrorMessage$1(claimMutation.error) }) })]
433
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$8.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$8.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$8.Description, { children: getErrorMessage$1(claimMutation.error) }) })]
434
434
  })
435
- ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalFooter$1, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$18, {
435
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalFooter$1, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$17, {
436
436
  variant: "secondary",
437
437
  onPress: close,
438
438
  children: "Cancel"
439
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$18, {
439
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$17, {
440
440
  type: "submit",
441
441
  isPending: claimMutation.isPending,
442
442
  children: "Verify and claim"
@@ -449,8 +449,42 @@ function getErrorMessage$1(error) {
449
449
  return error instanceof Error ? error.message : "Please try again.";
450
450
  }
451
451
  //#endregion
452
+ //#region frontend/src/useCommissioningVerification.ts
453
+ var { useQuery: useQuery$3 } = await importShared("@tanstack/react-query");
454
+ var { useEffect: useEffect$12, useState: useState$14 } = await importShared("react");
455
+ /** Enrollment evidence is independent of configuration and physical qualification. */
456
+ function useCommissioningVerification(session) {
457
+ const enabled = ["awaiting_verification", "completed"].includes(session.state);
458
+ const query = useQuery$3({
459
+ queryKey: [
460
+ "wago",
461
+ "commissioning-verification",
462
+ session.id
463
+ ],
464
+ queryFn: () => getCommissioningVerification(session.id),
465
+ enabled,
466
+ refetchInterval: 5e3
467
+ });
468
+ const [now, setNow] = useState$14(Date.now);
469
+ useEffect$12(() => {
470
+ if (!enabled) return;
471
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
472
+ return () => clearInterval(timer);
473
+ }, [enabled]);
474
+ const stale = !!query.data && now - query.dataUpdatedAt > 15e3;
475
+ const unavailable = query.isError || stale;
476
+ const data = enabled && !unavailable ? query.data : void 0;
477
+ const enrollmentComplete = !!(data?.controllerId && data.permanentConnection && data.enrollmentRevoked);
478
+ return {
479
+ data,
480
+ unavailable,
481
+ enrollmentComplete,
482
+ runtimeVerified: enrollmentComplete && data?.configurationApplied === true && data.hardwareReadiness === "ready"
483
+ };
484
+ }
485
+ //#endregion
452
486
  //#region frontend/src/ControllersTable.tsx
453
- var { Button: Button$17, Chip: Chip$2, Table, TableBody, TableCell, TableColumn, TableContent, TableHeader, TableRow, TableScrollContainer } = await importShared("@heroui/react");
487
+ var { Button: Button$16, Chip: Chip$2, Table, TableBody, TableCell, TableColumn, TableContent, TableHeader, TableRow, TableScrollContainer } = await importShared("@heroui/react");
454
488
  function ControllersTable({ controllers, sessions, onClaim, onConfigure, onRemove, onResume }) {
455
489
  const activeSessions = sessions.filter((session) => session.state !== "completed" && (session.state !== "revoked" || !!session.runtimeRecoveryAvailable || !!session.dockerProvisionState || !!session.managementControllerId));
456
490
  const rows = [...controllers.map((controller) => ({
@@ -540,23 +574,23 @@ function ControllerRow({ row, onClaim, onConfigure, onRemove, onResume }) {
540
574
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
541
575
  className: "wg:flex wg:justify-end wg:gap-2",
542
576
  children: [
543
- session && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$17, {
577
+ session && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$16, {
544
578
  size: "sm",
545
579
  variant: "secondary",
546
580
  onPress: () => onResume(session),
547
581
  children: "View progress"
548
582
  }),
549
- controller.trustState === "untrusted" ? !session && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$17, {
583
+ controller.trustState === "untrusted" ? !session && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$16, {
550
584
  size: "sm",
551
585
  onPress: () => onClaim(controller.id),
552
586
  children: "Claim"
553
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$17, {
587
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$16, {
554
588
  size: "sm",
555
589
  variant: "secondary",
556
590
  onPress: () => onConfigure(controller.id),
557
591
  children: "Configure"
558
592
  }),
559
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$17, {
593
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$16, {
560
594
  size: "sm",
561
595
  variant: "danger",
562
596
  onPress: () => onRemove(controller),
@@ -611,7 +645,7 @@ function CommissioningRow({ row, onResume }) {
611
645
  className: "wg:hidden wg:lg:table-cell",
612
646
  children: ["Updated ", new Date(session.updatedAt).toLocaleString()]
613
647
  }),
614
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$17, {
648
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$16, {
615
649
  size: "sm",
616
650
  variant: isResumable(session.state) ? "primary" : "secondary",
617
651
  onPress: () => onResume(session),
@@ -621,9 +655,11 @@ function CommissioningRow({ row, onResume }) {
621
655
  }, row.key);
622
656
  }
623
657
  function CommissioningStatus({ session }) {
658
+ const verification = useCommissioningVerification(session);
659
+ const label = verification.enrollmentComplete ? verification.runtimeVerified ? "Enrollment complete · runtime verified" : "Enrollment complete · runtime setup pending" : verification.unavailable ? "Verification status unavailable" : commissioningLabel(session.state);
624
660
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
625
661
  className: "wg:mt-1 wg:text-xs wg:text-primary",
626
- children: [commissioningLabel(session.state), session.failureReason ? `: ${session.failureReason}` : ""]
662
+ children: [label, session.failureReason ? `: ${session.failureReason}` : ""]
627
663
  });
628
664
  }
629
665
  function EmptyControllers() {
@@ -678,27 +714,27 @@ function formatHeartbeat(lastHeartbeatAt) {
678
714
  }
679
715
  //#endregion
680
716
  //#region frontend/src/RuntimeArtifactImport.tsx
681
- var { useEffect: useEffect$11, useRef: useRef$6, useState: useState$14 } = await importShared("react");
682
- var { Button: Button$16 } = await importShared("@heroui/react");
683
- var api$5 = createPluginApiClient("/api/wago/runtime-artifacts");
717
+ var { useEffect: useEffect$11, useRef: useRef$5, useState: useState$13 } = await importShared("react");
718
+ var { Button: Button$15 } = await importShared("@heroui/react");
719
+ var api$4 = createPluginApiClient("/api/wago/runtime-artifacts");
684
720
  var maxBytes = 536870912;
685
721
  /** Standalone admin panel; uses the host API origin and authenticated session automatically. */
686
722
  function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, disabled = false }) {
687
- const [current, setCurrent] = useState$14(null);
723
+ const [current, setCurrent] = useState$13(null);
688
724
  useEffect$11(() => {
689
725
  onSelectionChange?.(current);
690
726
  }, [current, onSelectionChange]);
691
- const [artifacts, setArtifacts] = useState$14([]);
692
- const [files, setFiles] = useState$14({});
693
- const [busy, setBusy] = useState$14(false);
694
- const [loading, setLoading] = useState$14(true);
695
- const [loadFailed, setLoadFailed] = useState$14(false);
696
- const [loadAttempt, setLoadAttempt] = useState$14(0);
697
- const [error, setError] = useState$14("");
698
- const [status, setStatus] = useState$14("");
699
- const form = useRef$6(null);
700
- const uploadAbort = useRef$6(null);
701
- const busyCallback = useRef$6(onBusyChange);
727
+ const [artifacts, setArtifacts] = useState$13([]);
728
+ const [files, setFiles] = useState$13({});
729
+ const [busy, setBusy] = useState$13(false);
730
+ const [loading, setLoading] = useState$13(true);
731
+ const [loadFailed, setLoadFailed] = useState$13(false);
732
+ const [loadAttempt, setLoadAttempt] = useState$13(0);
733
+ const [error, setError] = useState$13("");
734
+ const [status, setStatus] = useState$13("");
735
+ const form = useRef$5(null);
736
+ const uploadAbort = useRef$5(null);
737
+ const busyCallback = useRef$5(onBusyChange);
702
738
  useEffect$11(() => {
703
739
  busyCallback.current = onBusyChange;
704
740
  }, [onBusyChange]);
@@ -715,7 +751,7 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
715
751
  }, []);
716
752
  useEffect$11(() => {
717
753
  const abort = new AbortController();
718
- Promise.all([api$5.request("/current", { signal: abort.signal }), api$5.request("", { signal: abort.signal })]).then(([active, entries]) => {
754
+ Promise.all([api$4.request("/current", { signal: abort.signal }), api$4.request("", { signal: abort.signal })]).then(([active, entries]) => {
719
755
  if (abort.signal.aborted) return;
720
756
  setCurrent(active);
721
757
  setArtifacts(entries);
@@ -730,14 +766,14 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
730
766
  return () => abort.abort();
731
767
  }, [loadAttempt]);
732
768
  async function importRelease() {
733
- if (!files.bundle || !files.checksum || !files.signature || disabled || loading || loadFailed || uploadAbort.current) return;
734
- if (files.bundle.size > maxBytes || files.checksum.size > 4096 || files.signature.size > 16384) {
735
- setError("The runtime tar must be at most 512 MiB, checksum 4 KiB, and signature 16 KiB.");
769
+ if (!files.bundle || !files.checksum || disabled || loading || loadFailed || uploadAbort.current) return;
770
+ if (files.bundle.size > maxBytes || files.checksum.size > 4096) {
771
+ setError("The runtime tar must be at most 512 MiB and checksum 4 KiB.");
736
772
  return;
737
773
  }
738
774
  setBusy(true);
739
775
  setError("");
740
- setStatus("Uploading and verifying the signed release…");
776
+ setStatus("Uploading and checking the runtime release…");
741
777
  const abort = new AbortController();
742
778
  uploadAbort.current = abort;
743
779
  let failureMessage = "Import could not be completed. Check your connection and retry with the selected files.";
@@ -745,15 +781,14 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
745
781
  const body = new FormData();
746
782
  body.append("bundle", files.bundle);
747
783
  body.append("checksum", files.checksum);
748
- body.append("signature", files.signature);
749
- const response = await api$5.fetch("/import", {
784
+ const response = await api$4.fetch("/import", {
750
785
  method: "POST",
751
786
  body,
752
787
  signal: abort.signal
753
788
  });
754
789
  if (abort.signal.aborted) return;
755
790
  if (!response.ok) {
756
- failureMessage = response.status === 403 ? "Administrator access is required." : response.status === 409 ? "Another upload is in progress. Try again shortly." : "Import failed. Check that all three files belong to the same signed release and try again.";
791
+ failureMessage = response.status === 403 ? "Administrator access is required." : response.status === 409 ? "Another upload is in progress. Try again shortly." : "Import failed. Check that the runtime tar and checksum belong together and try again.";
757
792
  throw new Error("Import rejected");
758
793
  }
759
794
  const artifact = await response.json();
@@ -778,15 +813,15 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
778
813
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
779
814
  className: "wg:space-y-3",
780
815
  "aria-label": "CC100 runtime release",
781
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: "CC100 runtime release" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Download the signed WAGO runtime bundle, extract its archive, then select the three release files. No server paths or signing keys are needed." })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
816
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: "CC100 runtime release" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Download the WAGO runtime bundle, extract its archive, then select the runtime tar and checksum files." })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
782
817
  className: "wg:space-y-3",
783
818
  children: [
784
819
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
785
820
  href: "https://github.com/Attraccess/Attraccess/actions/workflows/wago-cc100-runtime.yml",
786
821
  target: "_blank",
787
822
  rel: "noreferrer",
788
- children: "Official signed runtime build artifacts"
789
- }), ". Your software distributor can also provide these files. Only signed builds are importable."] }),
823
+ children: "Official runtime build artifacts"
824
+ }), ". Your software distributor can also provide these files. Check the source before importing."] }),
790
825
  loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Loading releases…" }) : loadFailed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Runtime releases are unavailable." }) : current ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
791
826
  className: "wg:break-words",
792
827
  children: [
@@ -826,24 +861,16 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
826
861
  disabled: disabled || busy || loading || loadFailed,
827
862
  className: "wg:flex wg:flex-col wg:gap-3",
828
863
  children: [
829
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("legend", { children: "Signed release files" }),
830
- [
831
- [
832
- "bundle",
833
- "Runtime bundle (.tar)",
834
- ".tar"
835
- ],
836
- [
837
- "checksum",
838
- "Checksum (.sha256)",
839
- ".sha256"
840
- ],
841
- [
842
- "signature",
843
- "Signature (.sig)",
844
- ".sig"
845
- ]
846
- ].map(([field, label, accept]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [label, /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
864
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("legend", { children: "Runtime release files" }),
865
+ [[
866
+ "bundle",
867
+ "Runtime bundle (.tar)",
868
+ ".tar"
869
+ ], [
870
+ "checksum",
871
+ "Checksum (.sha256)",
872
+ ".sha256"
873
+ ]].map(([field, label, accept]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [label, /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
847
874
  type: "file",
848
875
  accept,
849
876
  required: true,
@@ -852,9 +879,9 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
852
879
  [field]: event.target.files?.[0]
853
880
  }))
854
881
  })] }, field)),
855
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$16, {
882
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
856
883
  type: "submit",
857
- isDisabled: disabled || busy || loading || loadFailed || !files.bundle || !files.checksum || !files.signature,
884
+ isDisabled: disabled || busy || loading || loadFailed || !files.bundle || !files.checksum,
858
885
  children: busy ? "Verifying release…" : "Import and select release"
859
886
  })
860
887
  ]
@@ -869,7 +896,7 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
869
896
  role: "alert",
870
897
  children: error
871
898
  }),
872
- loadFailed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$16, {
899
+ loadFailed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
873
900
  variant: "secondary",
874
901
  isDisabled: loading || busy,
875
902
  onPress: () => {
@@ -895,18 +922,18 @@ function RuntimeArtifactImport({ onImported, onBusyChange, onSelectionChange, di
895
922
  }
896
923
  //#endregion
897
924
  //#region frontend/src/ManagementSecurityStatus.tsx
898
- var { Alert: Alert$8, Button: Button$15, Checkbox: Checkbox$6, Form: Form$2, Input: Input$6, Label: Label$9, TextField: TextField$6 } = await importShared("@heroui/react");
899
- var { useEffect: useEffect$10, useRef: useRef$5, useState: useState$13 } = await importShared("react");
925
+ var { Alert: Alert$7, Button: Button$14, Checkbox: Checkbox$5, Form: Form$2, Input: Input$5, Label: Label$8, TextField: TextField$5 } = await importShared("@heroui/react");
926
+ var { useEffect: useEffect$10, useRef: useRef$4, useState: useState$12 } = await importShared("react");
900
927
  function ManagementSecurityStatus(props) {
901
- const [status, setStatus] = useState$13(props.status);
902
- const [mode, setMode] = useState$13("baseline");
903
- const [exceptions, setExceptions] = useState$13([]);
904
- const [confirmed, setConfirmed] = useState$13(false);
905
- const [credentialGeneration, setCredentialGeneration] = useState$13(0);
906
- const [pending, setPending] = useState$13(false);
907
- const [failed, setFailed] = useState$13(false);
908
- const form = useRef$5(null);
909
- const generation = useRef$5(0);
928
+ const [status, setStatus] = useState$12(props.status);
929
+ const [mode, setMode] = useState$12("baseline");
930
+ const [exceptions, setExceptions] = useState$12([]);
931
+ const [confirmed, setConfirmed] = useState$12(false);
932
+ const [credentialGeneration, setCredentialGeneration] = useState$12(0);
933
+ const [pending, setPending] = useState$12(false);
934
+ const [failed, setFailed] = useState$12(false);
935
+ const form = useRef$4(null);
936
+ const generation = useRef$4(0);
910
937
  useEffect$10(() => {
911
938
  setStatus(props.status);
912
939
  }, [props.status]);
@@ -979,34 +1006,34 @@ function ManagementSecurityStatus(props) {
979
1006
  onSubmit: (event) => event.preventDefault(),
980
1007
  "aria-label": "Management security actions",
981
1008
  children: [
982
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$6, {
1009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$5, {
983
1010
  name: "managementUsername",
984
1011
  isRequired: true,
985
1012
  isDisabled: pending,
986
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$9, { children: "Temporary SSH username" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$6, {
1013
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$8, { children: "Temporary SSH username" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$5, {
987
1014
  autoComplete: "off",
988
1015
  maxLength: 32
989
1016
  })]
990
1017
  }, `username-${credentialGeneration}`),
991
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$6, {
1018
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$5, {
992
1019
  name: "managementPassword",
993
1020
  isRequired: true,
994
1021
  isDisabled: pending,
995
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$9, { children: "Temporary SSH password" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$6, {
1022
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$8, { children: "Temporary SSH password" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$5, {
996
1023
  type: "password",
997
1024
  autoComplete: "off",
998
1025
  maxLength: 4096
999
1026
  })]
1000
1027
  }, `password-${credentialGeneration}`),
1001
1028
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Credentials are cleared after each request. Apply and recovery require fresh credentials." }),
1002
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
1029
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1003
1030
  type: "button",
1004
1031
  variant: "secondary",
1005
1032
  isDisabled: pending || !!recovery,
1006
1033
  onPress: () => void run("inspect"),
1007
1034
  children: "Inspect management"
1008
1035
  }),
1009
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
1036
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1010
1037
  type: "button",
1011
1038
  variant: mode === "baseline" ? "primary" : "secondary",
1012
1039
  isDisabled: pending || !!recovery,
@@ -1016,7 +1043,7 @@ function ManagementSecurityStatus(props) {
1016
1043
  },
1017
1044
  children: "Full baseline"
1018
1045
  }),
1019
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
1046
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1020
1047
  type: "button",
1021
1048
  variant: mode === "key_only" ? "primary" : "secondary",
1022
1049
  isDisabled: pending || !!recovery,
@@ -1039,16 +1066,16 @@ function ManagementSecurityStatus(props) {
1039
1066
  id: "unqualified_privileges",
1040
1067
  label: "Acknowledge unqualified account privileges (required for key enrollment)"
1041
1068
  }
1042
- ].map(({ id, label }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$6, {
1069
+ ].map(({ id, label }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$5, {
1043
1070
  isSelected: exceptions.includes(id),
1044
1071
  isDisabled: pending || !!recovery,
1045
1072
  onChange: (selected) => {
1046
1073
  setExceptions((current) => selected ? [...current.filter((value) => value !== id), id] : current.filter((value) => value !== id));
1047
1074
  setConfirmed(false);
1048
1075
  },
1049
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$6.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$6.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$6.Content, { children: label })]
1076
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Content, { children: label })]
1050
1077
  }, id)),
1051
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
1078
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1052
1079
  type: "button",
1053
1080
  variant: "secondary",
1054
1081
  isDisabled: pending || !status?.inspection || !!recovery,
@@ -1056,19 +1083,19 @@ function ManagementSecurityStatus(props) {
1056
1083
  children: "Review changes"
1057
1084
  }),
1058
1085
  reviewed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: mode === "key_only" ? "Review: snapshot authorized keys, arm rollback, add a unique key, verify a new pinned key connection, then retain the recovery snapshot. Existing access remains enabled." : "Review: snapshot access, arm reboot-safe rollback, add and verify a unique management key, restrict access, verify a new connection and the service baseline, then retain recovery." }),
1059
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$6, {
1086
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$5, {
1060
1087
  isSelected: confirmed,
1061
1088
  isDisabled: pending,
1062
1089
  onChange: setConfirmed,
1063
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$6.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$6.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$6.Content, { children: "I confirm the reviewed change or explicit recovery for this controller." })]
1090
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Content, { children: "I confirm the reviewed change or explicit recovery for this controller." })]
1064
1091
  }),
1065
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
1092
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1066
1093
  type: "button",
1067
1094
  isDisabled: pending || !confirmed || !reviewed || status?.support !== "supported",
1068
1095
  onPress: () => void run("apply"),
1069
1096
  children: "Apply reviewed change"
1070
1097
  }),
1071
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$15, {
1098
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1072
1099
  type: "button",
1073
1100
  variant: "secondary",
1074
1101
  isDisabled: pending || !confirmed || !recovery,
@@ -1098,9 +1125,9 @@ function ManagementSummary({ status, recovery }) {
1098
1125
  status?.support ?? "qualification_required"
1099
1126
  ]
1100
1127
  }),
1101
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$8, {
1128
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$7, {
1102
1129
  status: "warning",
1103
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$8.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$8.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$8.Description, { children: "Automatic FW31 management hardening is not implemented. Missing operations are a restricted management account, root/password restrictions, WBM and service restrictions, and recovery that survives reboot. The FW31 vendor procedure for applying, restoring and persisting these changes is still needed. Key enrollment supports an existing non-root OpenSSH account or a detected Dropbear 2025.88 account. The new key disables forwarding and PTY allocation; account privileges and existing access remain enabled. Exceptions never count as hardened. Physical hardware verification is separate." }) })]
1130
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$7.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$7.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$7.Description, { children: "Automatic FW31 management hardening is not implemented. Missing operations are a restricted management account, root/password restrictions, WBM and service restrictions, and recovery that survives reboot. The FW31 vendor procedure for applying, restoring and persisting these changes is still needed. Key enrollment supports an existing non-root OpenSSH account or a detected Dropbear 2025.88 account. The new key disables forwarding and PTY allocation; account privileges and existing access remain enabled. Exceptions never count as hardened. Physical hardware verification is separate." }) })]
1104
1131
  }),
1105
1132
  status?.inspection && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", { children: [
1106
1133
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: "Firmware / SSH / service control" }),
@@ -1137,16 +1164,16 @@ function ManagementSummary({ status, recovery }) {
1137
1164
  }
1138
1165
  //#endregion
1139
1166
  //#region frontend/src/CommissioningSecurityPanel.tsx
1140
- var { useEffect: useEffect$9, useRef: useRef$4, useState: useState$12 } = await importShared("react");
1141
- var api$4 = createPluginApiClient("/api/wago/commissioning/sessions");
1167
+ var { useEffect: useEffect$9, useRef: useRef$3, useState: useState$11 } = await importShared("react");
1168
+ var api$3 = createPluginApiClient("/api/wago/commissioning/sessions");
1142
1169
  function CommissioningSecurityPanel({ sessionId, controllerId }) {
1143
- const [status, setStatus] = useState$12(null);
1144
- const [loadError, setLoadError] = useState$12(false);
1145
- const generation = useRef$4(0);
1170
+ const [status, setStatus] = useState$11(null);
1171
+ const [loadError, setLoadError] = useState$11(false);
1172
+ const generation = useRef$3(0);
1146
1173
  useEffect$9(() => {
1147
1174
  const current = ++generation.current;
1148
1175
  const abort = new AbortController();
1149
- api$4.request(`/${sessionId}/management`, { signal: abort.signal }).then((value) => {
1176
+ api$3.request(`/${sessionId}/management`, { signal: abort.signal }).then((value) => {
1150
1177
  if (generation.current === current) setStatus(value);
1151
1178
  }).catch(() => {
1152
1179
  if (!abort.signal.aborted) setLoadError(true);
@@ -1158,7 +1185,7 @@ function CommissioningSecurityPanel({ sessionId, controllerId }) {
1158
1185
  }, [sessionId]);
1159
1186
  async function request(action, body) {
1160
1187
  const current = ++generation.current;
1161
- const value = await api$4.request(`/${sessionId}/management/${action}`, {
1188
+ const value = await api$3.request(`/${sessionId}/management/${action}`, {
1162
1189
  method: "POST",
1163
1190
  body
1164
1191
  });
@@ -1185,18 +1212,19 @@ function CommissioningSecurityPanel({ sessionId, controllerId }) {
1185
1212
  }
1186
1213
  //#endregion
1187
1214
  //#region frontend/src/CommissioningPlatformPreflight.tsx
1188
- var { Alert: Alert$7, Button: Button$14, Checkbox: Checkbox$5, Input: Input$5, Label: Label$8, TextField: TextField$5 } = await importShared("@heroui/react");
1189
- var { useEffect: useEffect$8, useRef: useRef$3, useState: useState$11 } = await importShared("react");
1215
+ var { Alert: Alert$6, Button: Button$13, Checkbox: Checkbox$4, Input: Input$4, Label: Label$7, TextField: TextField$4 } = await importShared("@heroui/react");
1216
+ var { useEffect: useEffect$8, useRef: useRef$2, useState: useState$10 } = await importShared("react");
1190
1217
  var { useQueryClient: useQueryClient$1 } = await importShared("@tanstack/react-query");
1191
- var api$3 = createPluginApiClient("/api/wago/commissioning/sessions");
1218
+ var api$2 = createPluginApiClient("/api/wago/commissioning/sessions");
1192
1219
  function CommissioningPlatformPreflight({ session }) {
1193
1220
  const client = useQueryClient$1();
1194
- const [updated, setUpdated] = useState$11(null);
1195
- const [busy, setBusy] = useState$11(false);
1196
- const [approved, setApproved] = useState$11(false);
1197
- const [error, setError] = useState$11("");
1198
- const form = useRef$3(null);
1199
- const generation = useRef$3(0);
1221
+ const [updated, setUpdated] = useState$10(null);
1222
+ const [busy, setBusy] = useState$10(false);
1223
+ const [approved, setApproved] = useState$10(false);
1224
+ const [customSsh, setCustomSsh] = useState$10(false);
1225
+ const [error, setError] = useState$10("");
1226
+ const form = useRef$2(null);
1227
+ const generation = useRef$2(0);
1200
1228
  useEffect$8(() => () => {
1201
1229
  generation.current++;
1202
1230
  form.current?.reset();
@@ -1212,16 +1240,17 @@ function CommissioningPlatformPreflight({ session }) {
1212
1240
  if (busy || !form.current?.reportValidity() || action !== "inspect" && !approved) return;
1213
1241
  const values = new FormData(form.current);
1214
1242
  const temporarySsh = {
1215
- username: String(values.get("preflightUsername") ?? ""),
1216
- password: String(values.get("preflightPassword") ?? "")
1243
+ username: customSsh ? String(values.get("preflightUsername") ?? "") : "root",
1244
+ password: customSsh ? String(values.get("preflightPassword") ?? "") : "wago"
1217
1245
  };
1218
1246
  form.current.reset();
1247
+ setCustomSsh(false);
1219
1248
  setApproved(false);
1220
1249
  setBusy(true);
1221
1250
  setError("");
1222
1251
  const request = generation.current;
1223
1252
  try {
1224
- const value = await api$3.request(`/${session.id}/platform/${action}`, {
1253
+ const value = await api$2.request(`/${session.id}/platform/${action}`, {
1225
1254
  method: "POST",
1226
1255
  body: {
1227
1256
  temporarySsh,
@@ -1233,7 +1262,7 @@ function CommissioningPlatformPreflight({ session }) {
1233
1262
  client.setQueryData(["wago", "commissioning-sessions"], (entries) => entries?.map((entry) => entry.id === value.id ? value : entry));
1234
1263
  }
1235
1264
  } catch {
1236
- if (request === generation.current) setError("Platform action failed. Recheck the supported action and fresh SSH credential.");
1265
+ if (request === generation.current) setError("Could not inspect or clean up the controller. Check SSH access and try again.");
1237
1266
  } finally {
1238
1267
  temporarySsh.password = "";
1239
1268
  if (request === generation.current) setBusy(false);
@@ -1243,8 +1272,8 @@ function CommissioningPlatformPreflight({ session }) {
1243
1272
  className: "wg:space-y-3",
1244
1273
  "aria-label": "Controller installation preflight",
1245
1274
  children: [
1246
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: "Controller installation preflight" }),
1247
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Optionally inspect firmware, UTC clock skew, digital I/O access, CODESYS and Docker before installation. Inspection does not change the controller. Installation checks these again under your destructive-install approval." }),
1275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: "Check the controller" }),
1276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Optional: check firmware and installation prerequisites. This does not change the controller." }),
1248
1277
  codesysDisabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1249
1278
  role: "status",
1250
1279
  children: "Controller preparation verified CODESYS stopped and permanently disabled. This is a saved result, not a live controller status check."
@@ -1296,9 +1325,9 @@ function CommissioningPlatformPreflight({ session }) {
1296
1325
  !codesysDisabled && report?.exclusivity === "codesys-active" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "CODESYS is active. Destructive installation will stop and permanently disable it. Existing PLC applications and data may be lost; Attraccess will not preserve, back up, or restore them. Installation fails if CODESYS cannot be verified stopped and disabled before I/O." }),
1297
1326
  !codesysDisabled && report?.exclusivity === "codesys-boot-enabled" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "CODESYS is configured to start at boot. Destructive installation must disable that startup and verify CODESYS is stopped before I/O. A stopped process alone is insufficient. No separate PLC preservation or restoration approval is required." }),
1298
1327
  report?.exclusivity === "output-container-conflict" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Another container can write the digital outputs. Resolve exclusive ownership before installing this runtime." }),
1299
- report?.provision === "unsupported-fw31-package-activation" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$7, {
1328
+ report?.provision === "unsupported-fw31-package-activation" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$6, {
1300
1329
  status: "warning",
1301
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$7.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$7.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$7.Description, { children: "This report could not establish a supported Docker package installation path. Retry installation only after the reported package or compatibility issue is resolved. An old inspection does not authorize activation by itself." }) })]
1330
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$6.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$6.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$6.Description, { children: "This report could not establish a supported Docker package installation path. Retry installation only after the reported package or compatibility issue is resolved. An old inspection does not authorize activation by itself." }) })]
1302
1331
  }),
1303
1332
  report?.provision === "unsupported-lifecycle-dependencies" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "This report could not verify the Docker lifecycle dependencies. Installation must validate a supported activation path before changing Docker. Vendor activation can change startup, routing and firewall settings; preexisting settings are not restored by commissioning." }),
1304
1333
  current.dockerProvisionState && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
@@ -1317,34 +1346,40 @@ function CommissioningPlatformPreflight({ session }) {
1317
1346
  ref: form,
1318
1347
  onSubmit: (event) => event.preventDefault(),
1319
1348
  children: [
1320
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$5, {
1349
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: ["SSH login: ", customSsh ? "Custom credentials" : "Default root account"] }),
1350
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$4, {
1351
+ isSelected: customSsh,
1352
+ onChange: setCustomSsh,
1353
+ isDisabled: busy,
1354
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Content, { children: "Advanced: use different SSH credentials" })]
1355
+ }),
1356
+ customSsh && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$4, {
1321
1357
  name: "preflightUsername",
1322
1358
  isRequired: true,
1323
1359
  isDisabled: busy,
1324
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$8, { children: "Preflight SSH username" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$5, { autoComplete: "off" })]
1325
- }),
1326
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$5, {
1360
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$7, { children: "Preflight SSH username" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$4, { autoComplete: "off" })]
1361
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$4, {
1327
1362
  name: "preflightPassword",
1328
1363
  isRequired: true,
1329
1364
  isDisabled: busy,
1330
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$8, { children: "Preflight SSH password" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$5, {
1365
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$7, { children: "Preflight SSH password" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$4, {
1331
1366
  type: "password",
1332
1367
  autoComplete: "off"
1333
1368
  })]
1334
- }),
1335
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1369
+ })] }),
1370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$13, {
1336
1371
  type: "button",
1337
1372
  variant: "secondary",
1338
1373
  isDisabled: busy,
1339
1374
  onPress: () => void run("inspect"),
1340
1375
  children: "Inspect installation prerequisites"
1341
1376
  }),
1342
- recovery && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$5, {
1377
+ recovery && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$4, {
1343
1378
  isSelected: approved,
1344
1379
  onChange: setApproved,
1345
1380
  isDisabled: busy,
1346
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$5.Content, { children: "I approve cleaning up this controller preparation. Preexisting workloads and host settings will not be restored." })]
1347
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$14, {
1381
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Content, { children: "I approve cleaning up this controller preparation. Preexisting workloads and host settings will not be restored." })]
1382
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$13, {
1348
1383
  type: "button",
1349
1384
  isDisabled: busy || !approved,
1350
1385
  onPress: () => void run("recover"),
@@ -1360,109 +1395,6 @@ function CommissioningPlatformPreflight({ session }) {
1360
1395
  });
1361
1396
  }
1362
1397
  //#endregion
1363
- //#region frontend/src/CommissioningOperationStatus.tsx
1364
- var { useQuery: useQuery$3 } = await importShared("@tanstack/react-query");
1365
- var { Alert: Alert$6, Button: Button$13, Checkbox: Checkbox$4, Input: Input$4, Label: Label$7, TextField: TextField$4 } = await importShared("@heroui/react");
1366
- var { useRef: useRef$2, useState: useState$10 } = await importShared("react");
1367
- var api$2 = createPluginApiClient("/api/wago/commissioning/sessions");
1368
- function CommissioningOperationStatus({ sessionId }) {
1369
- const query = useQuery$3({
1370
- queryKey: [
1371
- "wago",
1372
- "commissioning-operation",
1373
- sessionId
1374
- ],
1375
- queryFn: () => api$2.request(`/${sessionId}/operation`),
1376
- refetchInterval: 5e3
1377
- });
1378
- const [confirmed, setConfirmed] = useState$10(false);
1379
- const [busy, setBusy] = useState$10(false);
1380
- const [error, setError] = useState$10(false);
1381
- const form = useRef$2(null);
1382
- const status = query.data;
1383
- async function recover() {
1384
- if (!form.current?.reportValidity() || busy || !confirmed || status?.state !== "stale") return;
1385
- const fields = new FormData(form.current);
1386
- const temporarySsh = {
1387
- username: String(fields.get("leaseUsername") ?? ""),
1388
- password: String(fields.get("leasePassword") ?? "")
1389
- };
1390
- form.current.reset();
1391
- setConfirmed(false);
1392
- setBusy(true);
1393
- setError(false);
1394
- try {
1395
- await api$2.request(`/${sessionId}/operation/recover`, {
1396
- method: "POST",
1397
- body: {
1398
- temporarySsh,
1399
- owner: status.owner,
1400
- previousWorkerStopped: true
1401
- }
1402
- });
1403
- await query.refetch();
1404
- } catch {
1405
- setError(true);
1406
- } finally {
1407
- temporarySsh.password = "";
1408
- setBusy(false);
1409
- }
1410
- }
1411
- if (!status || status.state === "available") return null;
1412
- if (status.state !== "active" && status.state !== "stale") return null;
1413
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$6, {
1414
- status: "warning",
1415
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$6.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$6.Content, { children: [
1416
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$6.Title, { children: status.state === "active" ? "A commissioning operation owns this controller" : "Interrupted coordinator recovery required" }),
1417
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$6.Description, { children: "Another operation cannot change this controller until ownership is released. An application restart never silently takes over an interrupted operation." }),
1418
- status.state === "stale" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", {
1419
- ref: form,
1420
- onSubmit: (event) => {
1421
- event.preventDefault();
1422
- recover();
1423
- },
1424
- children: [
1425
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
1426
- "Recovery is available after ",
1427
- new Date(status.recoveryAfter).toLocaleString(),
1428
- ". Stop the previous Attraccess commissioning instance first. The server then checks that both controller locks are idle. Runtime and management snapshots are not removed."
1429
- ] }),
1430
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$4, {
1431
- name: "leaseUsername",
1432
- isRequired: true,
1433
- isDisabled: busy,
1434
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$7, { children: "Coordinator recovery SSH username" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$4, { autoComplete: "off" })]
1435
- }),
1436
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$4, {
1437
- name: "leasePassword",
1438
- isRequired: true,
1439
- isDisabled: busy,
1440
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$7, { children: "Coordinator recovery SSH password" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$4, {
1441
- type: "password",
1442
- autoComplete: "off"
1443
- })]
1444
- }),
1445
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$4, {
1446
- isSelected: confirmed,
1447
- onChange: setConfirmed,
1448
- isDisabled: busy,
1449
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$4.Content, { children: "The previous commissioning instance has stopped, not merely paused. I approve checking and releasing its expired operation." })]
1450
- }),
1451
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$13, {
1452
- type: "submit",
1453
- isDisabled: busy || !confirmed,
1454
- children: "Recover interrupted coordinator"
1455
- })
1456
- ]
1457
- }),
1458
- error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1459
- role: "alert",
1460
- children: "Recovery remains blocked. Check the safe timeout, previous instance and controller access, then retry."
1461
- })
1462
- ] })]
1463
- });
1464
- }
1465
- //#endregion
1466
1398
  //#region frontend/src/drawer.tsx
1467
1399
  var { DrawerBackdrop, DrawerContent, DrawerDialog } = await importShared("@heroui/react");
1468
1400
  var DRAWER_DIALOG_CLASSNAME = "wg:md:mx-auto wg:md:max-w-2xl wg:bg-surface-secondary";
@@ -1488,6 +1420,10 @@ var { Alert: Alert$5, Button: Button$12, Checkbox: Checkbox$3, DrawerBody: Drawe
1488
1420
  var { AlertCircleIcon, CheckCircle2Icon, CpuIcon: CpuIcon$1 } = await importShared("lucide-react");
1489
1421
  var { useEffect: useEffect$7, useState: useState$9 } = await importShared("react");
1490
1422
  var { useQuery: useQuery$2 } = await importShared("@tanstack/react-query");
1423
+ var DEFAULT_SSH = {
1424
+ username: "root",
1425
+ password: "wago"
1426
+ };
1491
1427
  function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onConfigure }) {
1492
1428
  const createSessionMutation = useCreateCommissioningSessionMutation();
1493
1429
  const confirmHostKeyMutation = useConfirmCommissioningHostKeyMutation();
@@ -1516,11 +1452,13 @@ function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onCon
1516
1452
  setIsolatedIdentity(false);
1517
1453
  setHostKeyFingerprint("");
1518
1454
  }, [resumedSession?.id, isOpen]);
1519
- const [sshUsername, setSshUsername] = useState$9("");
1520
- const [sshPassword, setSshPassword] = useState$9("");
1455
+ const [sshUsername, setSshUsername] = useState$9(DEFAULT_SSH.username);
1456
+ const [sshPassword, setSshPassword] = useState$9(DEFAULT_SSH.password);
1457
+ const [customSsh, setCustomSsh] = useState$9(false);
1521
1458
  const [confirmInstall, setConfirmInstall] = useState$9(false);
1522
- const [recoveryUsername, setRecoveryUsername] = useState$9("");
1523
- const [recoveryPassword, setRecoveryPassword] = useState$9("");
1459
+ const [recoveryUsername, setRecoveryUsername] = useState$9(DEFAULT_SSH.username);
1460
+ const [recoveryPassword, setRecoveryPassword] = useState$9(DEFAULT_SSH.password);
1461
+ const [customRecoverySsh, setCustomRecoverySsh] = useState$9(false);
1524
1462
  const [confirmRecovery, setConfirmRecovery] = useState$9(false);
1525
1463
  const [isCancelConfirmationOpen, setCancelConfirmationOpen] = useState$9(false);
1526
1464
  const attemptSession = recoverSessionMutation.submittedAt > deliverSessionMutation.submittedAt ? recoverSessionMutation.data : deliverSessionMutation.data;
@@ -1535,10 +1473,13 @@ function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onCon
1535
1473
  confirmHostKeyMutation
1536
1474
  });
1537
1475
  useEffect$7(() => {
1538
- setSshPassword("");
1476
+ setSshUsername(DEFAULT_SSH.username);
1477
+ setSshPassword(DEFAULT_SSH.password);
1478
+ setCustomSsh(false);
1539
1479
  setConfirmInstall(false);
1540
- setRecoveryUsername("");
1541
- setRecoveryPassword("");
1480
+ setRecoveryUsername(DEFAULT_SSH.username);
1481
+ setRecoveryPassword(DEFAULT_SSH.password);
1482
+ setCustomRecoverySsh(false);
1542
1483
  setConfirmRecovery(false);
1543
1484
  setHostKeyFingerprint("");
1544
1485
  setIsolatedIdentity(false);
@@ -1561,12 +1502,14 @@ function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onCon
1561
1502
  setControllerIp("");
1562
1503
  setMqttServerId(null);
1563
1504
  setHostKeyFingerprint("");
1564
- setSshUsername("");
1565
- setSshPassword("");
1505
+ setSshUsername(DEFAULT_SSH.username);
1506
+ setSshPassword(DEFAULT_SSH.password);
1507
+ setCustomSsh(false);
1566
1508
  setConfirmInstall(false);
1567
1509
  setCancelConfirmationOpen(false);
1568
- setRecoveryUsername("");
1569
- setRecoveryPassword("");
1510
+ setRecoveryUsername(DEFAULT_SSH.username);
1511
+ setRecoveryPassword(DEFAULT_SSH.password);
1512
+ setCustomRecoverySsh(false);
1570
1513
  setConfirmRecovery(false);
1571
1514
  createSessionMutation.reset();
1572
1515
  confirmHostKeyMutation.reset();
@@ -1581,11 +1524,10 @@ function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onCon
1581
1524
  createSessionMutation.mutate({
1582
1525
  name,
1583
1526
  targetHost: controllerIp,
1584
- mqttServerId: selectedMqttServerId,
1585
- runtimeArtifactDigest: selectedArtifact?.digest
1527
+ mqttServerId: selectedMqttServerId
1586
1528
  }, { onSuccess: (created) => {
1587
1529
  setCreatedSession(created);
1588
- setStep(2);
1530
+ setStep(3);
1589
1531
  } });
1590
1532
  }
1591
1533
  function deliverSession() {
@@ -1598,10 +1540,9 @@ function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onCon
1598
1540
  password: sshPassword
1599
1541
  }
1600
1542
  });
1601
- setSshPassword("");
1543
+ if (customSsh) setSshPassword("");
1602
1544
  setConfirmInstall(false);
1603
- setRecoveryUsername("");
1604
- setRecoveryPassword("");
1545
+ if (customRecoverySsh) setRecoveryPassword("");
1605
1546
  setConfirmRecovery(false);
1606
1547
  }
1607
1548
  function recoverSession() {
@@ -1614,10 +1555,9 @@ function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onCon
1614
1555
  password: recoveryPassword
1615
1556
  }
1616
1557
  });
1617
- setRecoveryUsername("");
1618
- setRecoveryPassword("");
1558
+ if (customRecoverySsh) setRecoveryPassword("");
1619
1559
  setConfirmRecovery(false);
1620
- setSshPassword("");
1560
+ if (customSsh) setSshPassword("");
1621
1561
  setConfirmInstall(false);
1622
1562
  }
1623
1563
  function confirmHostKey() {
@@ -1661,12 +1601,16 @@ function useCommissioning({ isOpen, session: resumedSession, onOpenChange, onCon
1661
1601
  sshPassword,
1662
1602
  setSshUsername,
1663
1603
  setSshPassword,
1604
+ customSsh,
1605
+ setCustomSsh,
1664
1606
  confirmInstall,
1665
1607
  setConfirmInstall,
1666
1608
  recoveryUsername,
1667
1609
  recoveryPassword,
1668
1610
  setRecoveryUsername,
1669
1611
  setRecoveryPassword,
1612
+ customRecoverySsh,
1613
+ setCustomRecoverySsh,
1670
1614
  confirmRecovery,
1671
1615
  setConfirmRecovery,
1672
1616
  createSessionMutation,
@@ -1701,7 +1645,7 @@ function CommissioningModal(props) {
1701
1645
  });
1702
1646
  }
1703
1647
  function CommissioningContent({ model }) {
1704
- const { onConfigure, session, activeStep, title, loadingStatus, hostKeyFingerprint, setHostKeyFingerprint, isolatedIdentity, setIsolatedIdentity, deliverSessionMutation, sshUsername, sshPassword, setSshUsername, setSshPassword, confirmInstall, setConfirmInstall, isCancelConfirmationOpen, configureController } = model;
1648
+ const { onConfigure, session, activeStep, title, loadingStatus, hostKeyFingerprint, setHostKeyFingerprint, isolatedIdentity, setIsolatedIdentity, deliverSessionMutation, sshUsername, sshPassword, setSshUsername, setSshPassword, customSsh, setCustomSsh, confirmInstall, setConfirmInstall, isCancelConfirmationOpen, configureController } = model;
1705
1649
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerBody$1, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1706
1650
  className: "wg:grid wg:min-w-0 wg:gap-5 wg:md:grid-cols-[13rem_minmax(0,1fr)]",
1707
1651
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DevicePassport, {
@@ -1716,16 +1660,7 @@ function CommissioningContent({ model }) {
1716
1660
  title: loadingStatus[0],
1717
1661
  description: loadingStatus[1]
1718
1662
  }),
1719
- session && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningOperationStatus, { sessionId: session.id }, `operation-${session.id}`),
1720
1663
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConnectionFields, { model }),
1721
- session?.runtimeArtifactDigest && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
1722
- className: "wg:break-all wg:text-sm",
1723
- children: [
1724
- "Pinned signed release: ",
1725
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: session.runtimeArtifactDigest }),
1726
- ". Retries keep this release."
1727
- ]
1728
- }),
1729
1664
  session?.state === "awaiting_identity_confirmation" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HostKeyConfirmationStep, {
1730
1665
  fingerprint: hostKeyFingerprint,
1731
1666
  expectedFingerprint: session.hostKeyFingerprint,
@@ -1736,21 +1671,31 @@ function CommissioningContent({ model }) {
1736
1671
  onChange: setIsolatedIdentity,
1737
1672
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Content, { children: "Alternatively, I verified the physical 751-9301 label and connected this controller as the only device on an isolated service network. I accept first-key pinning on that connection, not independent cryptographic identity verification. Do not select this on a shared LAN." })]
1738
1673
  }),
1739
- session && activeStep === 2 && session.state !== "awaiting_identity_confirmation" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryStep, {
1674
+ session && activeStep === 4 && session.state !== "awaiting_identity_confirmation" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeliveryStep, {
1740
1675
  isDelivering: deliverSessionMutation.isPending,
1741
1676
  session,
1742
1677
  sshUsername,
1743
1678
  sshPassword,
1744
1679
  onSshUsernameChange: setSshUsername,
1745
1680
  onSshPasswordChange: setSshPassword,
1681
+ customSsh,
1682
+ onCustomSshChange: (custom) => {
1683
+ setCustomSsh(custom);
1684
+ setSshUsername(custom ? "" : DEFAULT_SSH.username);
1685
+ setSshPassword(custom ? "" : DEFAULT_SSH.password);
1686
+ },
1746
1687
  confirmInstall,
1747
1688
  onConfirmInstallChange: setConfirmInstall
1748
1689
  }),
1749
- session && activeStep === 3 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProgressStep, {
1690
+ session && activeStep === 5 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProgressStep, {
1750
1691
  name: title,
1751
1692
  session
1752
1693
  }),
1753
- session && session.state !== "awaiting_identity_confirmation" && (canInstall(session) || session.dockerProvisionState) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningPlatformPreflight, { session }, `preflight-${session.id}`),
1694
+ session && canInstall(session) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", {
1695
+ className: "wg:cursor-pointer",
1696
+ children: "Optional: inspect controller before installation"
1697
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningPlatformPreflight, { session }, `preflight-${session.id}`)] }),
1698
+ session && !canInstall(session) && session.dockerProvisionState && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningPlatformPreflight, { session }, `preflight-${session.id}`),
1754
1699
  session && (["awaiting_verification", "completed"].includes(session.state) || session.managementControllerId) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VerificationStatus, {
1755
1700
  session,
1756
1701
  onConfigure: onConfigure ? configureController : void 0
@@ -1801,13 +1746,13 @@ function CommissioningActions({ model }) {
1801
1746
  });
1802
1747
  }
1803
1748
  function RecoveryFields({ model }) {
1804
- const { session, isLoading, recoveryUsername, recoveryPassword, setRecoveryUsername, setRecoveryPassword, confirmRecovery, setConfirmRecovery } = model;
1749
+ const { session, isLoading, recoveryUsername, recoveryPassword, setRecoveryUsername, setRecoveryPassword, customRecoverySsh, setCustomRecoverySsh, confirmRecovery, setConfirmRecovery } = model;
1805
1750
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: session && canRecover(session) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1806
1751
  className: "wg:space-y-4",
1807
1752
  children: [
1808
1753
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5, {
1809
1754
  status: "warning",
1810
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Clean up failed installation" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: "Cleanup interrupts the Attraccess runtime and reconciles this installation and its credentials. It cannot undo broker credential revocation or restore CODESYS, other applications, Docker host settings, or data erased during commissioning. It does not re-enable CODESYS. An incomplete cleanup keeps its recovery record for another attempt. Cleanup does not certify readiness and is never automatic." })] })]
1755
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Clean up failed installation" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: "This stops and removes the failed Attraccess installation. It cannot restore previous applications, data, or CODESYS. You can retry after cleanup." })] })]
1811
1756
  }),
1812
1757
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CredentialFields, {
1813
1758
  intent: "recovery",
@@ -1815,7 +1760,13 @@ function RecoveryFields({ model }) {
1815
1760
  username: recoveryUsername,
1816
1761
  password: recoveryPassword,
1817
1762
  onUsernameChange: setRecoveryUsername,
1818
- onPasswordChange: setRecoveryPassword
1763
+ onPasswordChange: setRecoveryPassword,
1764
+ custom: customRecoverySsh,
1765
+ onCustomChange: (custom) => {
1766
+ setCustomRecoverySsh(custom);
1767
+ setRecoveryUsername(custom ? "" : DEFAULT_SSH.username);
1768
+ setRecoveryPassword(custom ? "" : DEFAULT_SSH.password);
1769
+ }
1819
1770
  }),
1820
1771
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$3, {
1821
1772
  isRequired: true,
@@ -1824,10 +1775,6 @@ function RecoveryFields({ model }) {
1824
1775
  onChange: setConfirmRecovery,
1825
1776
  name: "confirm-recovery",
1826
1777
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Content, { children: "I approve interrupting the Attraccess runtime and cleaning up this failed installation. This does not restore preexisting applications or data." })]
1827
- }),
1828
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1829
- className: "wg:text-sm wg:text-muted",
1830
- children: "Enter fresh SSH credentials and approve each recovery attempt separately. Credentials and recovery consent are cleared after submission or closing."
1831
1778
  })
1832
1779
  ]
1833
1780
  }) });
@@ -1846,7 +1793,7 @@ function ConnectionFields({ model }) {
1846
1793
  onControllerIpChange: setControllerIp,
1847
1794
  onMqttServerIdChange: setMqttServerId
1848
1795
  }),
1849
- !session && activeStep === 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RuntimeArtifactImport, {
1796
+ !session && activeStep === 2 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RuntimeArtifactImport, {
1850
1797
  disabled: isLoading,
1851
1798
  onBusyChange: setArtifactBusy,
1852
1799
  onSelectionChange: setSelectedArtifact
@@ -1855,19 +1802,32 @@ function ConnectionFields({ model }) {
1855
1802
  }
1856
1803
  function CreateSessionActions({ model }) {
1857
1804
  const { session, isLoading, activeStep, name, controllerIp, mqttServersQuery, setArtifactBusy, artifactBusy, artifactAvailable, selectedMqttServerId, setStep, createSession } = model;
1858
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [!session && activeStep === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
1859
- isDisabled: !name.trim(),
1860
- onPress: () => {
1861
- setArtifactBusy(true);
1862
- setStep(1);
1863
- },
1864
- children: "Continue"
1865
- }), !session && activeStep === 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
1866
- isPending: isLoading,
1867
- isDisabled: artifactBusy || !artifactAvailable || !controllerIp.trim() || selectedMqttServerId === null || mqttServersQuery.isPending || mqttServersQuery.isError,
1868
- onPress: createSession,
1869
- children: isLoading ? "Preparing commissioning" : "Scan controller for review"
1870
- })] });
1805
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1806
+ !session && activeStep === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
1807
+ isDisabled: !name.trim(),
1808
+ onPress: () => {
1809
+ setArtifactBusy(true);
1810
+ setStep(1);
1811
+ },
1812
+ children: "Continue"
1813
+ }),
1814
+ !session && activeStep === 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
1815
+ isDisabled: !controllerIp.trim() || selectedMqttServerId === null || mqttServersQuery.isPending || mqttServersQuery.isError,
1816
+ onPress: () => setStep(2),
1817
+ children: "Continue"
1818
+ }),
1819
+ !session && activeStep === 2 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
1820
+ variant: "secondary",
1821
+ onPress: () => setStep(1),
1822
+ children: "Back to connection"
1823
+ }),
1824
+ !session && activeStep === 2 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
1825
+ isPending: isLoading,
1826
+ isDisabled: artifactBusy || !artifactAvailable || !controllerIp.trim() || selectedMqttServerId === null || mqttServersQuery.isPending || mqttServersQuery.isError,
1827
+ onPress: createSession,
1828
+ children: isLoading ? "Preparing commissioning" : "Scan controller for review"
1829
+ })
1830
+ ] });
1871
1831
  }
1872
1832
  function CancelSessionAction({ model }) {
1873
1833
  const { session, isLoading, removeSessionMutation, isCancelConfirmationOpen, setCancelConfirmationOpen, close } = model;
@@ -1914,15 +1874,15 @@ function DevicePassport({ className, name, step }) {
1914
1874
  children: [
1915
1875
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PassportRow, {
1916
1876
  label: "Identity",
1917
- value: step >= 2 ? "Review required" : "Not scanned"
1877
+ value: step >= 3 ? "See session status" : "Not scanned"
1918
1878
  }),
1919
1879
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PassportRow, {
1920
1880
  label: "Runtime",
1921
- value: step >= 2 ? "See session status" : "Not installed"
1881
+ value: step >= 4 ? "See session status" : "Not installed"
1922
1882
  }),
1923
1883
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PassportRow, {
1924
1884
  label: "Claim",
1925
- value: step >= 3 ? "See session status" : "Pending"
1885
+ value: step >= 5 ? "See session status" : "Pending"
1926
1886
  })
1927
1887
  ]
1928
1888
  }),
@@ -1932,7 +1892,9 @@ function DevicePassport({ className, name, step }) {
1932
1892
  0,
1933
1893
  1,
1934
1894
  2,
1935
- 3
1895
+ 3,
1896
+ 4,
1897
+ 5
1936
1898
  ].map((index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `wg:h-1.5 wg:flex-1 wg:rounded-full ${index <= step ? "wg:bg-primary" : "wg:bg-default-300"}` }, index))
1937
1899
  })
1938
1900
  ]
@@ -1949,10 +1911,12 @@ function PassportRow({ label, value }) {
1949
1911
  }
1950
1912
  function StepHeading({ step }) {
1951
1913
  const content = [
1952
- ["Give this controller a useful name", "This name is reserved now and applied automatically when the controller comes online."],
1953
- ["Connect the controller", "Enter its private address and choose an MQTT server. Review the scanned identity before installation."],
1954
- ["Review and approve installation", "Confirm the controller identity, enter temporary SSH credentials, and approve the changes for this attempt."],
1955
- ["Commissioning status", "A saved session does not authorize a new installation. After an interruption, review the status before retrying."]
1914
+ ["Name the controller", "Choose a name you will recognize later."],
1915
+ ["Connect the controller", "Enter its IP address and choose the MQTT server it will use."],
1916
+ ["Choose a runtime release", "Select the release to install on this controller."],
1917
+ ["Verify the controller", "Check the SSH fingerprint before installation."],
1918
+ ["Install the runtime", "Review what installation changes, then approve it."],
1919
+ ["Installation progress", "You can close this window and return to the saved session later."]
1956
1920
  ][step];
1957
1921
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1958
1922
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
@@ -1960,7 +1924,7 @@ function StepHeading({ step }) {
1960
1924
  children: [
1961
1925
  "Step ",
1962
1926
  step + 1,
1963
- " of 4"
1927
+ " of 6"
1964
1928
  ]
1965
1929
  }),
1966
1930
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
@@ -2017,7 +1981,7 @@ function ConnectionStep({ controllerIp, mqttServerId, mqttServersQuery, onContro
2017
1981
  children: [
2018
1982
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5, {
2019
1983
  status: "accent",
2020
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Prepare the CC100 (751-9301), firmware 31" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: "Have the controller powered using its specified supply and wait for it to finish starting. Connect its Ethernet cable to the intended local network. Ask your network administrator for its assigned IP address and ensure the Attraccess server can reach it. Keep power and networking connected during installation. If the model or firmware is uncertain, check the device label and your maintenance records with the responsible administrator before proceeding." })] })]
1984
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Prepare the CC100 (751-9301), firmware 31" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: "Power on the CC100 and connect it to the local network. Keep it connected during installation. Check the device label and firmware if you are unsure which controller this is." })] })]
2021
1985
  }),
2022
1986
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$3, {
2023
1987
  isRequired: true,
@@ -2068,7 +2032,7 @@ function HostKeyConfirmationStep({ fingerprint, expectedFingerprint, onFingerpri
2068
2032
  children: [
2069
2033
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5, {
2070
2034
  status: "warning",
2071
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Review the controller SSH key" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: "Compare the scanned fingerprint with a trusted inventory record or a fingerprint obtained independently through a trusted administrator. Copying the scan back here is not independent authentication. If no trusted fingerprint is available, confirm the physical controller and its cabling on an isolated network you control; this reduces exposure but does not independently authenticate the SSH key. Stop if the device or network is uncertain. Confirming the key does not start installation." })] })]
2035
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Review the controller SSH key" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: "Compare this fingerprint with a trusted record. If you do not have one, connect only this controller to an isolated service network and confirm its physical label. Stop if you cannot identify the controller." })] })]
2072
2036
  }),
2073
2037
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
2074
2038
  className: "wg:break-all wg:text-sm",
@@ -2095,33 +2059,48 @@ function canInstall(session) {
2095
2059
  function canRecover(session) {
2096
2060
  return session.runtimeRecoveryAvailable === true;
2097
2061
  }
2098
- function CredentialFields({ intent = "installation", isDisabled, username, password, onUsernameChange, onPasswordChange }) {
2062
+ function CredentialFields({ intent = "installation", isDisabled, username, password, onUsernameChange, onPasswordChange, custom, onCustomChange }) {
2099
2063
  const prefix = intent === "recovery" ? "Recovery SSH" : "Temporary SSH";
2100
2064
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2101
- className: "wg:grid wg:gap-4 wg:sm:grid-cols-2",
2102
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$3, {
2103
- isRequired: true,
2104
- isDisabled,
2105
- name: `${intent}-ssh-username`,
2106
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Label$6, { children: [prefix, " username"] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$3, {
2107
- autoComplete: "off",
2108
- value: username,
2109
- onChange: (event) => onUsernameChange(event.target.value)
2110
- })]
2111
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$3, {
2112
- isRequired: true,
2113
- isDisabled,
2114
- name: `${intent}-ssh-password`,
2115
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Label$6, { children: [prefix, " password"] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$3, {
2116
- autoComplete: "off",
2117
- type: "password",
2118
- value: password,
2119
- onChange: (event) => onPasswordChange(event.target.value)
2120
- })]
2121
- })]
2065
+ className: "wg:space-y-3",
2066
+ children: [
2067
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
2068
+ className: "wg:text-sm",
2069
+ children: ["SSH login: ", custom ? "Custom credentials" : "Default root account"]
2070
+ }),
2071
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$3, {
2072
+ isSelected: custom,
2073
+ isDisabled,
2074
+ onChange: onCustomChange,
2075
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Indicator, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$3.Content, { children: "Advanced: use different SSH credentials" })]
2076
+ }),
2077
+ custom && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2078
+ className: "wg:grid wg:gap-4 wg:sm:grid-cols-2",
2079
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$3, {
2080
+ isRequired: true,
2081
+ isDisabled,
2082
+ name: `${intent}-ssh-username`,
2083
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Label$6, { children: [prefix, " username"] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$3, {
2084
+ autoComplete: "off",
2085
+ value: username,
2086
+ onChange: (event) => onUsernameChange(event.target.value)
2087
+ })]
2088
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField$3, {
2089
+ isRequired: true,
2090
+ isDisabled,
2091
+ name: `${intent}-ssh-password`,
2092
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Label$6, { children: [prefix, " password"] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$3, {
2093
+ autoComplete: "off",
2094
+ type: "password",
2095
+ value: password,
2096
+ onChange: (event) => onPasswordChange(event.target.value)
2097
+ })]
2098
+ })]
2099
+ })
2100
+ ]
2122
2101
  });
2123
2102
  }
2124
- function DeliveryStep({ isDelivering, session, sshUsername, sshPassword, onSshUsernameChange, onSshPasswordChange, confirmInstall, onConfirmInstallChange }) {
2103
+ function DeliveryStep({ isDelivering, session, sshUsername, sshPassword, onSshUsernameChange, onSshPasswordChange, customSsh, onCustomSshChange, confirmInstall, onConfirmInstallChange }) {
2125
2104
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2126
2105
  className: "wg:space-y-4",
2127
2106
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningStatusPanel, {
@@ -2131,9 +2110,10 @@ function DeliveryStep({ isDelivering, session, sshUsername, sshPassword, onSshUs
2131
2110
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5, {
2132
2111
  status: "warning",
2133
2112
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Destructive installation" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Description, { children: [
2134
- "Installing Attraccess on ",
2113
+ "Make connected equipment safe before continuing. Installation stops and disables CODESYS on",
2114
+ " ",
2135
2115
  session.targetHost,
2136
- " takes over this controller. Existing applications and workloads may stop working or be erased. CODESYS will be stopped and permanently disabled before digital I/O is enabled; installation fails if this cannot be verified. Attraccess does not preserve, back up, or restore preexisting CODESYS applications or other workloads. Make connected equipment safe for the interruption. Installation does not certify management hardening or physical readiness."
2116
+ ". Existing applications and data may be lost and cannot be restored by Attraccess."
2137
2117
  ] })] })]
2138
2118
  }),
2139
2119
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CredentialFields, {
@@ -2141,7 +2121,9 @@ function DeliveryStep({ isDelivering, session, sshUsername, sshPassword, onSshUs
2141
2121
  username: sshUsername,
2142
2122
  password: sshPassword,
2143
2123
  onUsernameChange: onSshUsernameChange,
2144
- onPasswordChange: onSshPasswordChange
2124
+ onPasswordChange: onSshPasswordChange,
2125
+ custom: customSsh,
2126
+ onCustomChange: onCustomSshChange
2145
2127
  }),
2146
2128
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox$3, {
2147
2129
  isRequired: true,
@@ -2153,29 +2135,35 @@ function DeliveryStep({ isDelivering, session, sshUsername, sshPassword, onSshUs
2153
2135
  }),
2154
2136
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
2155
2137
  className: "wg:text-sm wg:text-muted",
2156
- children: "Enter the controller credentials explicitly; no default credentials are used. The password and approval are cleared after submitting or closing. Every retry needs a new approval and password. Restarting the server does not start or resume an installation."
2138
+ children: "Each attempt needs your approval. Custom passwords are cleared after use."
2157
2139
  })
2158
2140
  ] })]
2159
2141
  });
2160
2142
  }
2161
2143
  function ProgressStep({ name, session }) {
2162
- const complete = [
2144
+ const verification = useCommissioningVerification(session);
2145
+ const complete = verification.enrollmentComplete || [
2163
2146
  "completed",
2164
2147
  "revoked",
2165
2148
  "claim_interrupted",
2166
2149
  "recovery_revocation_pending"
2167
2150
  ].includes(session.state);
2151
+ const progress = verification.enrollmentComplete ? {
2152
+ ...session,
2153
+ progressStep: "Runtime enrollment complete",
2154
+ progressDetail: verification.runtimeVerified ? "Permanent MQTT access, enrollment credential revocation, applied configuration and a fresh runtime probe are verified." : "Permanent MQTT access and enrollment credential revocation are verified. Configure inputs and outputs to finish runtime setup."
2155
+ } : session;
2168
2156
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2169
2157
  className: "wg:space-y-4",
2170
2158
  children: [
2171
2159
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DevicePassport, {
2172
2160
  className: "wg:md:hidden",
2173
2161
  name,
2174
- step: 3
2162
+ step: 5
2175
2163
  }),
2176
2164
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningStatusPanel, {
2177
2165
  isActive: !complete,
2178
- session
2166
+ session: progress
2179
2167
  }),
2180
2168
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2181
2169
  className: "wg:rounded-large wg:border wg:border-default-200 wg:p-4 wg:text-sm",
@@ -2195,34 +2183,33 @@ function ProgressStep({ name, session }) {
2195
2183
  });
2196
2184
  }
2197
2185
  function VerificationStatus({ session, onConfigure }) {
2198
- const verification = useQuery$2({
2199
- queryKey: [
2200
- "wago",
2201
- "commissioning-verification",
2202
- session.id
2203
- ],
2204
- queryFn: () => getCommissioningVerification(session.id),
2205
- refetchInterval: 5e3
2206
- });
2186
+ const verification = useCommissioningVerification(session);
2187
+ const controllerId = verification.data?.controllerId;
2188
+ const managementControllerId = controllerId ?? session.managementControllerId;
2207
2189
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2208
2190
  className: "wg:space-y-3",
2209
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5, {
2210
- status: "warning",
2211
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: "Commissioning is not yet verified" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: verification.isError ? "Verification could not be loaded. No readiness claim is made." : verification.data ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { children: [
2212
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Permanent heartbeat: ", verification.data.permanentConnection ? "received" : "pending"] }),
2213
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Enrollment credential revoked: ", verification.data.enrollmentRevoked ? "verified" : "pending"] }),
2214
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Desired/reported configuration: ", verification.data.configurationApplied ? "applied" : "pending"] }),
2215
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Runtime hardware probe: ", verification.data.hardwareReadiness ?? "unverified"] }),
2216
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Management hardening: ", verification.data.managementHardening] }),
2217
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "Physical qualification: required before production use" })
2218
- ] }) : "Checking commissioning evidence..." })] })]
2219
- }), (verification.data?.controllerId || session.managementControllerId) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [onConfigure && verification.data?.controllerId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
2220
- onPress: () => onConfigure(verification.data.controllerId),
2221
- children: "Configure inputs and outputs"
2222
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningSecurityPanel, {
2223
- sessionId: session.id,
2224
- controllerId: verification.data?.controllerId ?? session.managementControllerId
2225
- }, session.id)] })]
2191
+ children: [
2192
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5, {
2193
+ status: verification.enrollmentComplete ? "success" : "warning",
2194
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Title, { children: verification.enrollmentComplete ? "Enrollment complete" : "Verifying enrollment" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$5.Description, { children: verification.unavailable ? "Current verification is unavailable. Waiting for a fresh status check." : verification.data ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Permanent heartbeat: ", verification.data.permanentConnection ? "received" : "pending"] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Enrollment credential revoked: ", verification.data.enrollmentRevoked ? "verified" : "pending"] })] }) : "Checking commissioning evidence..." })] })]
2195
+ }),
2196
+ verification.data && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
2197
+ "aria-label": "Runtime setup and qualification",
2198
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: verification.runtimeVerified ? "Runtime configuration verified" : "Runtime setup checks" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { children: [
2199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Desired/reported configuration: ", verification.data.configurationApplied ? "applied" : "pending"] }),
2200
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Runtime hardware probe: ", verification.data.hardwareReadiness ?? "unverified"] }),
2201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: ["Management hardening: ", verification.data.managementHardening] }),
2202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "Physical qualification: required before production use" })
2203
+ ] })]
2204
+ }),
2205
+ managementControllerId && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [onConfigure && controllerId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$12, {
2206
+ onPress: () => onConfigure(controllerId),
2207
+ children: "Configure inputs and outputs"
2208
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningSecurityPanel, {
2209
+ sessionId: session.id,
2210
+ controllerId: managementControllerId
2211
+ }, session.id)] })
2212
+ ]
2226
2213
  });
2227
2214
  }
2228
2215
  function CommissioningStatusPanel({ isActive, session }) {
@@ -2230,7 +2217,7 @@ function CommissioningStatusPanel({ isActive, session }) {
2230
2217
  const isQueued = session.state === "awaiting_delivery";
2231
2218
  const hasFailure = !isActive && Boolean(session.failureReason);
2232
2219
  const title = isQueued ? "Installation approval required" : session.progressStep ?? (isActive ? "Preparing commissioning" : commissioningLabel(session.state));
2233
- const detail = isQueued ? "Review the installation, enter temporary SSH credentials, and approve this attempt. Saved sessions do not start or resume installation automatically." : session.progressDetail ?? "Waiting for the next commissioning operation.";
2220
+ const detail = isQueued ? "Review the installation and approve this attempt. Saved sessions do not start automatically." : session.progressDetail ?? "Waiting for the next commissioning operation.";
2234
2221
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
2235
2222
  "aria-live": "polite",
2236
2223
  className: "wg:rounded-large wg:border wg:border-primary/30 wg:bg-primary/5 wg:p-4",
@@ -2312,13 +2299,13 @@ function parseActivityLog(auditLog) {
2312
2299
  }
2313
2300
  function sessionStep(session) {
2314
2301
  if (!session) return 0;
2302
+ if (session.state === "awaiting_identity_confirmation") return 3;
2315
2303
  return [
2316
2304
  "awaiting_delivery",
2317
2305
  "delivering",
2318
- "awaiting_identity_confirmation",
2319
2306
  "awaiting_codesys_confirmation",
2320
2307
  "delivery_failed"
2321
- ].includes(session.state) ? 2 : 3;
2308
+ ].includes(session.state) ? 4 : 5;
2322
2309
  }
2323
2310
  function ErrorAlert$1({ error }) {
2324
2311
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$5, {
@@ -3568,7 +3555,7 @@ var base = {
3568
3555
  pollIntervalMs: 5e3,
3569
3556
  functionCode: 3
3570
3557
  };
3571
- var BUILTIN_MODBUS_PROFILES = ["879-3000", "879-1300"].map((model) => ({
3558
+ var legacyProfiles = ["879-3000", "879-1300"].map((model) => ({
3572
3559
  id: `wago-${model}-unverified`,
3573
3560
  name: `WAGO ${model} — UNQUALIFIED / map unverified`,
3574
3561
  version: 1,
@@ -3599,6 +3586,40 @@ var BUILTIN_MODBUS_PROFILES = ["879-3000", "879-1300"].map((model) => ({
3599
3586
  kind: "cumulative"
3600
3587
  }))]
3601
3588
  }));
3589
+ var BUILTIN_MODBUS_PROFILES = [{
3590
+ id: "wago-879-3000",
3591
+ name: "WAGO 879-3000 — Modbus RTU",
3592
+ version: 1,
3593
+ actions: [],
3594
+ measurements: [
3595
+ ...legacyProfiles[0].measurements.map((measurement) => ({
3596
+ ...measurement,
3597
+ decimalPlaces: 3
3598
+ })),
3599
+ {
3600
+ ...base,
3601
+ id: "voltage-l1",
3602
+ name: "L1 voltage",
3603
+ address: 20482,
3604
+ dataType: "float32",
3605
+ scale: 1,
3606
+ unit: "volt",
3607
+ kind: "live",
3608
+ decimalPlaces: 3
3609
+ },
3610
+ {
3611
+ ...base,
3612
+ id: "current-l1",
3613
+ name: "L1 current",
3614
+ address: 20492,
3615
+ dataType: "float32",
3616
+ scale: 1,
3617
+ unit: "ampere",
3618
+ kind: "live",
3619
+ decimalPlaces: 3
3620
+ }
3621
+ ]
3622
+ }, ...legacyProfiles];
3602
3623
  function freeze(value) {
3603
3624
  Object.values(value).forEach((child) => {
3604
3625
  if (child && typeof child === "object") freeze(child);
@@ -3746,7 +3767,8 @@ function validateModbus(value) {
3746
3767
  "unit",
3747
3768
  "kind",
3748
3769
  "pollIntervalMs",
3749
- "rollover"
3770
+ "rollover",
3771
+ "decimalPlaces"
3750
3772
  ] : ["onValue", "offValue"]
3751
3773
  ], `${path}.${f.id}`);
3752
3774
  });
@@ -3757,7 +3779,7 @@ function validateModbus(value) {
3757
3779
  "watt",
3758
3780
  "watt-hour",
3759
3781
  "percent"
3760
- ].includes(m.unit) || !["live", "cumulative"].includes(m.kind) || !integer(m.pollIntervalMs, 100, 36e5)) fail(path, "measurement requires FC03/04, physical unit, kind, poll interval 100..3600000ms");
3782
+ ].includes(m.unit) || !["live", "cumulative"].includes(m.kind) || m.decimalPlaces !== void 0 && !integer(m.decimalPlaces, 0, 3) || !integer(m.pollIntervalMs, 100, 36e5)) fail(path, "measurement requires FC03/04, physical unit, kind, poll interval 100..3600000ms");
3761
3783
  if (m?.rollover !== void 0 && (m.kind !== "cumulative" || !Number.isFinite(m.rollover) || m.rollover <= 0)) fail(path, "rollover must be an explicit positive raw modulus on cumulative measurements");
3762
3784
  if (m?.kind === "cumulative" && m.scale <= 0) fail(path, "cumulative measurements require a positive scale");
3763
3785
  });
@@ -4340,6 +4362,26 @@ function ModbusProfileForm({ value, onChange, isDisabled = false, showIdentifier
4340
4362
  });
4341
4363
  }
4342
4364
  }),
4365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Choice$1, {
4366
+ label: "Reading precision (physical units)",
4367
+ value: m.decimalPlaces ?? "exact",
4368
+ options: [
4369
+ "exact",
4370
+ 0,
4371
+ 1,
4372
+ 2,
4373
+ 3
4374
+ ],
4375
+ labels: {
4376
+ exact: "Exact (no rounding)",
4377
+ 0: "Whole units",
4378
+ 1: "0.1 units",
4379
+ 2: "0.01 units",
4380
+ 3: "0.001 units"
4381
+ },
4382
+ disabled: readonly,
4383
+ onChange: (v) => update({ decimalPlaces: v === "exact" ? void 0 : Number(v) })
4384
+ }),
4343
4385
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
4344
4386
  label: "Polling interval (ms)",
4345
4387
  value: m.pollIntervalMs,
@@ -4563,7 +4605,7 @@ function ModbusConfigurationForm({ value, onChange, isDisabled = false, showIden
4563
4605
  ] }),
4564
4606
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
4565
4607
  className: "wg:text-sm wg:text-muted",
4566
- children: "Built-in register maps are unverified candidates. Check the device manual and qualify the hardware before use."
4608
+ children: "WAGO 879-3000 uses its documented read-only register map. Profiles marked unverified are retained for existing configurations."
4567
4609
  }),
4568
4610
  value.connections.map((c, index) => {
4569
4611
  if (focused && (section !== "connections" || c.id !== selectedId)) return null;
@@ -4609,8 +4651,8 @@ function ModbusConfigurationForm({ value, onChange, isDisabled = false, showIden
4609
4651
  } : {
4610
4652
  transport: "rtu",
4611
4653
  path: "/dev/serial",
4612
- baudRate: 19200,
4613
- parity: "even",
4654
+ baudRate: 9600,
4655
+ parity: "none",
4614
4656
  stopBits: 1
4615
4657
  }
4616
4658
  } : item)
@@ -4628,6 +4670,10 @@ function ModbusConfigurationForm({ value, onChange, isDisabled = false, showIden
4628
4670
  disabled: isDisabled,
4629
4671
  onChange: (v) => update({ port: Number(v) })
4630
4672
  })] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
4673
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
4674
+ className: "wg:text-sm wg:text-muted",
4675
+ children: "CC100 two-wire RS-485: /dev/serial. WAGO 879-3000 factory settings: unit 1, 9600 baud, 8N1."
4676
+ }),
4631
4677
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
4632
4678
  label: "Serial device path",
4633
4679
  value: c.path,