iobroker.eos-admin 7.9.36 → 7.9.38

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.
@@ -3321,7 +3321,7 @@ html.eos-app #eos-assist-root {
3321
3321
  }
3322
3322
 
3323
3323
 
3324
- /* === NexoWatt EOS v36: native adapter configuration safe mode =============
3324
+ /* === NexoWatt EOS v38: native adapter configuration safe mode =============
3325
3325
  Custom adapter configuration pages (React/HTML/jsonConfig) must be fully
3326
3326
  controlled by the adapter itself. EOS shell decoration is disabled inside the
3327
3327
  content area so buttons such as "Gerät hinzufügen" and "Gerät bearbeiten" stay
@@ -3371,3 +3371,84 @@ html.eos-app.eos-adapter-config-surface #app-paper .eos-protected-adapter-row {
3371
3371
  filter: none !important;
3372
3372
  pointer-events: auto !important;
3373
3373
  }
3374
+
3375
+
3376
+ /* === NexoWatt EOS v38: native popup/dialog compatibility ==================
3377
+ EOS decoration must never block native Admin dialogs, adapter install search,
3378
+ autocomplete poppers, menus or adapter-owned configuration dialogs. */
3379
+ html.eos-app .MuiDialog-root,
3380
+ html.eos-app .MuiModal-root,
3381
+ html.eos-app .MuiPopover-root,
3382
+ html.eos-app .MuiPopper-root,
3383
+ html.eos-app .MuiMenu-root,
3384
+ html.eos-app .MuiAutocomplete-popper,
3385
+ html.eos-app [role="dialog"],
3386
+ html.eos-app [role="listbox"],
3387
+ html.eos-app [role="menu"] {
3388
+ pointer-events: auto !important;
3389
+ }
3390
+ html.eos-app .MuiDialog-paper,
3391
+ html.eos-app .MuiPaper-root[role="dialog"],
3392
+ html.eos-app .MuiPopover-paper,
3393
+ html.eos-app .MuiMenu-paper,
3394
+ html.eos-app .MuiAutocomplete-paper,
3395
+ html.eos-app .MuiAutocomplete-listbox,
3396
+ html.eos-app .MuiList-root[role="listbox"] {
3397
+ pointer-events: auto !important;
3398
+ user-select: auto !important;
3399
+ }
3400
+ html.eos-app .MuiAutocomplete-popper,
3401
+ html.eos-app .MuiPopper-root,
3402
+ html.eos-app .MuiPopover-root,
3403
+ html.eos-app .MuiMenu-root {
3404
+ z-index: 6500 !important;
3405
+ }
3406
+ html.eos-app .MuiDialog-root button,
3407
+ html.eos-app .MuiDialog-root [role="button"],
3408
+ html.eos-app .MuiDialog-root a,
3409
+ html.eos-app .MuiModal-root button,
3410
+ html.eos-app .MuiModal-root [role="button"],
3411
+ html.eos-app .MuiModal-root a,
3412
+ html.eos-app .MuiPopover-root button,
3413
+ html.eos-app .MuiPopover-root [role="button"],
3414
+ html.eos-app .MuiPopover-root a,
3415
+ html.eos-app .MuiPopper-root button,
3416
+ html.eos-app .MuiPopper-root [role="button"],
3417
+ html.eos-app .MuiPopper-root a,
3418
+ html.eos-app .MuiMenu-root button,
3419
+ html.eos-app .MuiMenu-root [role="button"],
3420
+ html.eos-app .MuiMenu-root a,
3421
+ html.eos-app [role="listbox"] [role="option"],
3422
+ html.eos-app .MuiAutocomplete-option {
3423
+ pointer-events: auto !important;
3424
+ visibility: visible !important;
3425
+ }
3426
+
3427
+ /* v38: snackbars/toasts are clickable, but generic dialogs are not modified. */
3428
+ html.eos-app .MuiSnackbar-root,
3429
+ html.eos-app .SnackbarItem-root,
3430
+ html.eos-app .SnackbarItem-wrappedRoot,
3431
+ html.eos-app .notistack-Snackbar,
3432
+ html.eos-app .Toastify__toast-container,
3433
+ html.eos-app .Toastify__toast {
3434
+ pointer-events: auto !important;
3435
+ z-index: 5200 !important;
3436
+ }
3437
+ html.eos-app .MuiSnackbar-root button,
3438
+ html.eos-app .SnackbarItem-root button,
3439
+ html.eos-app .notistack-Snackbar button,
3440
+ html.eos-app .Toastify__toast button {
3441
+ pointer-events: auto !important;
3442
+ visibility: visible !important;
3443
+ opacity: 1 !important;
3444
+ }
3445
+
3446
+ /* v38: never allow decorative EOS layers to sit above native modals/popups. */
3447
+ html.eos-app .eos-top-toolbar::before,
3448
+ html.eos-app .eos-top-toolbar::after,
3449
+ html.eos-app .eos-brand-badge::before,
3450
+ html.eos-app .eos-brand-badge::after,
3451
+ html.eos-app .eos-panel::before,
3452
+ html.eos-app .eos-panel::after {
3453
+ pointer-events: none !important;
3454
+ }
@@ -31,7 +31,7 @@
31
31
  rel="stylesheet"
32
32
  href="css/leaflet.css"
33
33
  />
34
- <link rel="stylesheet" href="./css/eos-branding.css?v=36" />
34
+ <link rel="stylesheet" href="./css/eos-branding.css?v=38" />
35
35
  <link
36
36
  rel="manifest"
37
37
  href="manifest.json"
@@ -154,9 +154,9 @@
154
154
  <script type="module" crossorigin src="./assets/index-CQZugZ1z.js"></script>
155
155
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js">
156
156
  <link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js">
157
- <script defer src="./js/eos-branding.js?v=36"></script>
158
- <script defer src="./js/eos-security-ui.js?v=36"></script>
159
- <script defer src="./js/eos-assistant.js?v=36"></script>
157
+ <script defer src="./js/eos-branding.js?v=38"></script>
158
+ <script defer src="./js/eos-security-ui.js?v=38"></script>
159
+ <script defer src="./js/eos-assistant.js?v=38"></script>
160
160
  </head>
161
161
  <body>
162
162
  <noscript>You need to enable JavaScript to run this app.</noscript>
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- window.NEXOWATT_EOS_UI_VERSION = 'v36-native-config-session-fix';
4
+ window.NEXOWATT_EOS_UI_VERSION = 'v38-popup-config-stability-fix';
5
5
 
6
6
  const BRAND = 'NexoWatt EOS';
7
7
  const EOS_MEANING = 'Energy Operation System';
@@ -48,6 +48,46 @@
48
48
  'ß': 'ß', 'Ä': 'Ä', 'Ö': 'Ö', 'Ü': 'Ü', 'ä': 'ä', 'ö': 'ö', 'ü': 'ü'
49
49
  }));
50
50
 
51
+
52
+
53
+ const decodeMojibakeChunk = chunk => {
54
+ try {
55
+ const bytes = Uint8Array.from(Array.from(chunk, char => char.charCodeAt(0) & 0xff));
56
+ const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
57
+ return decoded && decoded !== chunk ? decoded : chunk;
58
+ } catch (_) {
59
+ return chunk;
60
+ }
61
+ };
62
+
63
+ const repairMojibake = value => {
64
+ let text = String(value || '');
65
+ // Repair common UTF-8-as-Latin1 fragments, including double-encoded strings
66
+ // such as dürfen, Löschen, geschützt.
67
+ for (let i = 0; i < 3 && /(?:Ã.|Â.|â.|�)/.test(text); i += 1) {
68
+ const repaired = text.replace(/[\u00C2-\u00F4][\u0080-\u00BF\u00A0-\u00BF]+/g, decodeMojibakeChunk);
69
+ if (repaired === text) break;
70
+ text = repaired;
71
+ }
72
+ const hardMap = new Map(Object.entries({
73
+ 'dürfen': 'dürfen', 'dürfen': 'dürfen', 'Dürfen': 'Dürfen', 'Dürfen': 'Dürfen',
74
+ 'für': 'für', 'für': 'für', 'Für': 'Für', 'Für': 'Für',
75
+ 'können': 'können', 'können': 'können', 'Können': 'Können', 'Können': 'Können',
76
+ 'möglich': 'möglich', 'möglich': 'möglich', 'Möglich': 'Möglich', 'Möglich': 'Möglich',
77
+ 'Löschen': 'Löschen', 'Löschen': 'Löschen', 'löschen': 'löschen', 'löschen': 'löschen',
78
+ 'schützen': 'schützen', 'schützen': 'schützen', 'Schützen': 'Schützen', 'Schützen': 'Schützen',
79
+ 'Geschützte': 'Geschützte', 'Geschützte': 'Geschützte', 'geschützte': 'geschützte', 'geschützte': 'geschützte',
80
+ 'ausgewählte': 'ausgewählte', 'ausgewählte': 'ausgewählte', 'Ausgewählte': 'Ausgewählte', 'Ausgewählte': 'Ausgewählte',
81
+ 'ändern': 'ändern', 'ändern': 'ändern', 'Über': 'Über', 'Über': 'Über', 'über': 'über', 'über': 'über',
82
+ 'Gerät': 'Gerät', 'Gerät': 'Gerät', 'Geräte': 'Geräte', 'Geräte': 'Geräte',
83
+ 'schließen': 'schließen', 'schließen': 'schließen', 'ß': 'ß', 'ß': 'ß',
84
+ 'ä': 'ä', 'ä': 'ä', 'ö': 'ö', 'ö': 'ö', 'ü': 'ü', 'ü': 'ü',
85
+ 'Ä': 'Ä', 'Ä': 'Ä', 'Ö': 'Ö', 'Ö': 'Ö', 'Ü': 'Ü', 'Ü': 'Ü', ' ': ' ', 'Â': ''
86
+ }));
87
+ for (const [from, to] of hardMap) if (text.includes(from)) text = text.split(from).join(to);
88
+ return text;
89
+ };
90
+
51
91
  const EXACT_LABELS = new Map(Object.entries({
52
92
  'Admin': BRAND,
53
93
  'NEXOWATT': 'NEXOWATT EOS',
@@ -104,11 +144,13 @@
104
144
 
105
145
  const replaceBrand = value => {
106
146
  if (!value || typeof value !== 'string') return value;
107
- let next = value;
147
+ let next = repairMojibake(value);
108
148
  for (const [from, to] of MOJIBAKE_REPLACEMENTS) {
109
149
  if (next.includes(from)) next = next.split(from).join(to);
110
150
  }
151
+ next = repairMojibake(next);
111
152
  for (const [pattern, replacement] of TEXT_REPLACEMENTS) next = next.replace(pattern, replacement);
153
+ next = repairMojibake(next);
112
154
  const compact = next.trim();
113
155
  if (EXACT_LABELS.has(compact)) next = next.replace(compact, EXACT_LABELS.get(compact));
114
156
  return next;
@@ -145,6 +187,32 @@
145
187
  while ((node = walker.nextNode())) patchTextNode(node);
146
188
  });
147
189
 
190
+
191
+ const patchMojibakeTextNode = node => {
192
+ if (!node || node.nodeType !== Node.TEXT_NODE || !node.nodeValue) return;
193
+ if (skipElement(node.parentElement)) return;
194
+ const before = node.nodeValue;
195
+ const after = repairMojibake(before);
196
+ if (after !== before) node.nodeValue = after;
197
+ };
198
+
199
+ const patchMojibakeTextNodes = root => safe(() => {
200
+ if (!root) return;
201
+ if (root.nodeType === Node.TEXT_NODE) {
202
+ patchMojibakeTextNode(root);
203
+ return;
204
+ }
205
+ if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return;
206
+ if (skipElement(root)) return;
207
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
208
+ acceptNode(node) {
209
+ return skipElement(node.parentElement) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
210
+ }
211
+ });
212
+ let node;
213
+ while ((node = walker.nextNode())) patchMojibakeTextNode(node);
214
+ });
215
+
148
216
  const patchImage = img => {
149
217
  const src = img.getAttribute('src') || '';
150
218
  const alt = img.getAttribute('alt') || '';
@@ -335,6 +403,52 @@
335
403
  });
336
404
  };
337
405
 
406
+
407
+ const releaseNotificationControls = () => safe(() => {
408
+ // v38: Keep native snackbar/toast close buttons clickable without touching dialogs,
409
+ // poppers, menus or adapter-owned configuration surfaces. Earlier broad selectors
410
+ // changed generic dialogs and broke React click handlers.
411
+ const roots = [
412
+ '.MuiSnackbar-root',
413
+ '.SnackbarItem-root',
414
+ '.SnackbarItem-wrappedRoot',
415
+ '.notistack-Snackbar',
416
+ '.Toastify__toast-container',
417
+ '.Toastify__toast'
418
+ ].join(',');
419
+ document.querySelectorAll(roots).forEach(box => {
420
+ if (box.closest('.MuiDialog-root,.MuiModal-root,.MuiPopover-root,.MuiPopper-root,.MuiMenu-root,[role="dialog"]')) return;
421
+ box.classList.add('eos-notification-safe');
422
+ box.style.pointerEvents = 'auto';
423
+ box.querySelectorAll('button, [role="button"], a, .MuiIconButton-root').forEach(control => {
424
+ control.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete');
425
+ // Do not remove disabled states globally. Only restore pointer handling.
426
+ control.style.pointerEvents = 'auto';
427
+ control.style.visibility = '';
428
+ control.style.opacity = '';
429
+ });
430
+ });
431
+ });
432
+
433
+ const ensurePopupCompatibility = () => safe(() => {
434
+ // v38: All native Admin dialogs, adapter install/autocomplete poppers, menus and
435
+ // adapter-owned modals must stay above EOS decorative layers and keep their React
436
+ // event handlers untouched.
437
+ const selectors = [
438
+ '.MuiDialog-root', '.MuiModal-root', '.MuiPopover-root', '.MuiPopper-root',
439
+ '.MuiMenu-root', '.MuiAutocomplete-popper', '.MuiAutocomplete-listbox',
440
+ '[role="dialog"]', '[role="listbox"]', '[role="menu"]'
441
+ ].join(',');
442
+ document.querySelectorAll(selectors).forEach(el => {
443
+ el.classList.add('eos-native-popup-safe');
444
+ if (el.style) {
445
+ el.style.pointerEvents = 'auto';
446
+ const isFloating = el.matches('.MuiPopover-root,.MuiPopper-root,.MuiMenu-root,.MuiAutocomplete-popper,[role="listbox"],[role="menu"]');
447
+ if (isFloating && !el.closest('.MuiDialog-paper')) el.style.zIndex = '6500';
448
+ }
449
+ });
450
+ });
451
+
338
452
  const protectDeleteDialogs = () => {
339
453
  if (isAdminUser() || state.securityPolicy.restrictProtectedAdapterControls === false) return;
340
454
  const protectedAdapters = state.securityPolicy.protectedAdapters || [];
@@ -398,6 +512,7 @@
398
512
  const applySecurityUiGuard = () => safe(() => {
399
513
  const policy = state.securityPolicy;
400
514
  applySecurityClasses();
515
+ releaseNotificationControls();
401
516
  // Do not apply EOS security decoration inside native adapter configuration pages.
402
517
  // Adapter UIs must remain 100% functional; backend/role checks still protect EOS actions.
403
518
  if (isAdapterConfigSurface()) return;
@@ -747,6 +862,8 @@
747
862
  }
748
863
  patchDrawerHeader(document.querySelector('.MuiDrawer-paper'));
749
864
  hideNativeLogoutNav();
865
+ releaseNotificationControls();
866
+ ensurePopupCompatibility();
750
867
  removeLogoutButton();
751
868
  });
752
869
 
@@ -867,6 +984,26 @@
867
984
  });
868
985
 
869
986
 
987
+
988
+
989
+ const ensureNotificationDialogClasses = () => safe(() => {
990
+ document.querySelectorAll('.MuiDialog-root, .MuiModal-root, [role="presentation"]').forEach(root => {
991
+ const paper = root.querySelector?.('.MuiDialog-paper, [role="dialog"]');
992
+ if (!paper) return;
993
+ const txt = normalize(paper.textContent || '');
994
+ if (!/(benachrichtigungen|notifications|acknowledge|bestätigen|schließen|close)/i.test(txt) && !paper.querySelector('#notifications-dialog-close')) return;
995
+ root.classList.add('eos-notification-dialog-root');
996
+ paper.classList.add('eos-notification-dialog');
997
+ paper.querySelectorAll('button, [role="button"], a, .MuiButtonBase-root, .MuiIconButton-root').forEach(control => {
998
+ control.style.pointerEvents = 'auto';
999
+ control.style.userSelect = 'auto';
1000
+ if (control.getAttribute('aria-disabled') === 'true' && /schließen|close/i.test(control.textContent || control.getAttribute('aria-label') || control.getAttribute('title') || '')) {
1001
+ control.removeAttribute('aria-disabled');
1002
+ }
1003
+ });
1004
+ });
1005
+ });
1006
+
870
1007
  const ensureSettingsDialogClasses = () => safe(() => {
871
1008
  const dialogs = Array.from(document.querySelectorAll('.MuiDialog-paper, [role="dialog"]'));
872
1009
  dialogs.forEach(dialog => {
@@ -920,6 +1057,15 @@
920
1057
  });
921
1058
 
922
1059
 
1060
+
1061
+
1062
+ const patchNotifications = () => safe(() => {
1063
+ // Kept for compatibility with older calls. v38 intentionally scopes this to
1064
+ // snackbar/toast surfaces only; no dialogs, popovers or adapter config controls.
1065
+ releaseNotificationControls();
1066
+ ensurePopupCompatibility();
1067
+ });
1068
+
923
1069
  const applyNavCompactPreference = () => safe(() => {
924
1070
  const compact = localStorage.getItem('nexowatt:eosNavCompact') === '1';
925
1071
  document.documentElement.classList.toggle('eos-nav-compact', compact);
@@ -1223,10 +1369,18 @@
1223
1369
  ensureRightsHelper();
1224
1370
  ensurePermissionPresets();
1225
1371
  ensureSettingsDialogClasses();
1372
+ ensurePopupCompatibility();
1226
1373
  hideNativeLogoutNav();
1227
1374
  hideOfficialNexoWattRepoWarning();
1375
+ releaseNotificationControls();
1376
+ ensurePopupCompatibility();
1228
1377
  applySecurityUiGuard();
1229
1378
  if (isAdapterConfigSurface()) {
1379
+ // Adapter-owned configuration pages must not be rebranded or structurally patched.
1380
+ // We still repair broken UTF-8/mojibake text because jsonConfig labels can be
1381
+ // rendered through different legacy paths. This is text-only and does not touch
1382
+ // adapter controls, React state, events or attributes.
1383
+ patchMojibakeTextNodes(document.getElementById('app-paper'));
1230
1384
  ['.MuiAppBar-root', '.MuiDrawer-paper', 'nav', '.eos-brand-badge', '.eos-top-toolbar'].forEach(selector => {
1231
1385
  document.querySelectorAll(selector).forEach(scope => {
1232
1386
  patchTextNodes(scope);
@@ -1255,12 +1409,18 @@
1255
1409
  ensureRightsHelper();
1256
1410
  ensurePermissionPresets();
1257
1411
  ensureSettingsDialogClasses();
1412
+ ensurePopupCompatibility();
1258
1413
  hideNativeLogoutNav();
1259
1414
  hideOfficialNexoWattRepoWarning();
1415
+ releaseNotificationControls();
1416
+ ensurePopupCompatibility();
1260
1417
  applySecurityUiGuard();
1261
1418
  for (const scope of scopes.slice(0, 80)) {
1262
1419
  if (!scope || !scope.isConnected) continue;
1263
- if (isAdapterConfigSurface() && (scope.id === 'app-paper' || scope.closest?.('#app-paper'))) continue;
1420
+ if (isAdapterConfigSurface() && (scope.id === 'app-paper' || scope.closest?.('#app-paper'))) {
1421
+ patchMojibakeTextNodes(scope);
1422
+ continue;
1423
+ }
1264
1424
  patchTextNodes(scope);
1265
1425
  patchAttributes(scope);
1266
1426
  }
@@ -1291,14 +1451,17 @@
1291
1451
  const observer = new MutationObserver(mutations => {
1292
1452
  for (const mutation of mutations) {
1293
1453
  if (mutation.type === 'characterData') {
1294
- patchTextNode(mutation.target);
1454
+ if (isAdapterConfigSurface() && mutation.target?.parentElement?.closest?.('#app-paper')) patchMojibakeTextNode(mutation.target);
1455
+ else patchTextNode(mutation.target);
1295
1456
  continue;
1296
1457
  }
1297
1458
  if (mutation.type !== 'childList') continue;
1298
1459
  mutation.addedNodes.forEach(node => {
1299
1460
  if (!node) return;
1300
- if (node.nodeType === Node.TEXT_NODE) patchTextNode(node);
1301
- else if (node.nodeType === Node.ELEMENT_NODE) state.pendingScopes.add(node);
1461
+ if (node.nodeType === Node.TEXT_NODE) {
1462
+ if (isAdapterConfigSurface() && node.parentElement?.closest?.('#app-paper')) patchMojibakeTextNode(node);
1463
+ else patchTextNode(node);
1464
+ } else if (node.nodeType === Node.ELEMENT_NODE) state.pendingScopes.add(node);
1302
1465
  });
1303
1466
  }
1304
1467
  if (state.pendingScopes.size) scheduleScopePatch();
@@ -0,0 +1,144 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ window.NEXOWATT_EOS_RUNTIME_FIXES_VERSION = 'v38-popup-config-stability-fix';
5
+
6
+ const MOJIBAKE_MAP = new Map(Object.entries({
7
+ 'dürfen': 'dürfen', 'Dürfen': 'Dürfen',
8
+ 'für': 'für', 'Für': 'Für',
9
+ 'müssen': 'müssen', 'Müssen': 'Müssen',
10
+ 'können': 'können', 'Können': 'Können',
11
+ 'möglich': 'möglich', 'Möglich': 'Möglich',
12
+ 'Löschen': 'Löschen', 'löschen': 'löschen',
13
+ 'schützen': 'schützen', 'Schützen': 'Schützen',
14
+ 'Schützt': 'Schützt', 'schützt': 'schützt',
15
+ 'Geschützte': 'Geschützte', 'geschützte': 'geschützte',
16
+ 'Geschützter': 'Geschützter', 'geschützter': 'geschützter',
17
+ 'geschützten': 'geschützten', 'Geschützten': 'Geschützten',
18
+ 'ausgewählte': 'ausgewählte', 'Ausgewählte': 'Ausgewählte',
19
+ 'ändern': 'ändern', 'Ändern': 'Ändern',
20
+ 'über': 'über', 'Über': 'Über',
21
+ 'Wähle': 'Wähle', 'wähle': 'wähle',
22
+ 'öffnen': 'öffnen', 'Öffnen': 'Öffnen',
23
+ 'schließen': 'schließen', 'Schließen': 'Schließen',
24
+ 'Gerät': 'Gerät', 'gerät': 'gerät',
25
+ 'Geräte': 'Geräte', 'geräte': 'geräte',
26
+ 'Zugänge': 'Zugänge', 'zugänge': 'zugänge',
27
+ 'Sicherheitsgründen': 'Sicherheitsgründen',
28
+ 'Kompatibilitätsgründen': 'Kompatibilitätsgründen',
29
+ 'Benachrichtigungen': 'Benachrichtigungen',
30
+ 'ß': 'ß', 'Ä': 'Ä', 'Ö': 'Ö', 'Ü': 'Ü', 'ä': 'ä', 'ö': 'ö', 'ü': 'ü',
31
+ '–': '–', '—': '—', '„': '„', '“': '“', '”': '”', ' ': ' ', 'Â': ''
32
+ }));
33
+
34
+ const safe = fn => { try { return fn(); } catch { return undefined; } };
35
+
36
+ const repairText = value => {
37
+ let text = String(value || '');
38
+ if (!/[ÃÂâ]/.test(text)) return text;
39
+ for (const [from, to] of MOJIBAKE_MAP) {
40
+ if (text.includes(from)) text = text.split(from).join(to);
41
+ }
42
+ return text;
43
+ };
44
+
45
+ const skip = el => {
46
+ const tag = el?.tagName;
47
+ return tag === 'SCRIPT' || tag === 'STYLE' || tag === 'TEXTAREA' || tag === 'CODE' || tag === 'PRE';
48
+ };
49
+
50
+ const repairSecurityText = root => safe(() => {
51
+ const base = root && root.nodeType ? root : document.body || document.documentElement;
52
+ if (!base) return;
53
+ const walker = document.createTreeWalker(base, NodeFilter.SHOW_TEXT, {
54
+ acceptNode(node) {
55
+ return skip(node.parentElement) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
56
+ }
57
+ });
58
+ let node;
59
+ while ((node = walker.nextNode())) {
60
+ const before = node.nodeValue || '';
61
+ const after = repairText(before);
62
+ if (after !== before) node.nodeValue = after;
63
+ }
64
+ const attrSelector = '[title],[aria-label],[placeholder],[value]';
65
+ const elements = base.nodeType === Node.ELEMENT_NODE && base.matches?.(attrSelector)
66
+ ? [base]
67
+ : Array.from(base.querySelectorAll?.(attrSelector) || []);
68
+ elements.forEach(el => {
69
+ if (skip(el)) return;
70
+ ['title', 'aria-label', 'placeholder', 'value'].forEach(attr => {
71
+ if (!el.hasAttribute?.(attr)) return;
72
+ const before = el.getAttribute(attr) || '';
73
+ const after = repairText(before);
74
+ if (after !== before) el.setAttribute(attr, after);
75
+ });
76
+ });
77
+ });
78
+
79
+ const isNotificationSurface = el => !!el?.closest?.([
80
+ '.MuiSnackbar-root',
81
+ '.SnackbarItem-root',
82
+ '.SnackbarItem-wrappedRoot',
83
+ '.notistack-Snackbar',
84
+ '.Toastify__toast',
85
+ '.eos-notification-safe'
86
+ ].join(','));
87
+
88
+ const isCloseControl = el => {
89
+ const label = `${el?.textContent || ''} ${el?.getAttribute?.('aria-label') || ''} ${el?.getAttribute?.('title') || ''}`.toLowerCase();
90
+ return /close|schlie(?:ß|ss)en|dismiss|ausblenden|ok|verstanden|x$/.test(label)
91
+ || !!el?.querySelector?.('svg[data-testid*="Close"], svg[data-testid*="Clear"], .material-icons');
92
+ };
93
+
94
+ const restoreCloseButton = button => safe(() => {
95
+ if (!button || !isCloseControl(button)) return;
96
+ button.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete', 'eos-hidden-logout', 'eos-native-logout-hidden');
97
+ button.removeAttribute('aria-disabled');
98
+ button.removeAttribute('data-eos-security-blocked');
99
+ if ('disabled' in button && button.disabled && !button.dataset.eosOriginalDisabled) button.disabled = false;
100
+ button.style.pointerEvents = 'auto';
101
+ button.style.visibility = 'visible';
102
+ button.style.opacity = '1';
103
+ if (button.style.display === 'none') button.style.display = '';
104
+ });
105
+
106
+ const repairNotifications = root => safe(() => {
107
+ const base = root && root.nodeType ? root : document;
108
+ const surfaces = Array.from(base.querySelectorAll?.([
109
+ '.MuiSnackbar-root',
110
+ '.SnackbarItem-root',
111
+ '.SnackbarItem-wrappedRoot',
112
+ '.notistack-Snackbar',
113
+ '.Toastify__toast'
114
+ ].join(',')) || []);
115
+ if (base.nodeType === Node.ELEMENT_NODE && isNotificationSurface(base)) surfaces.push(base);
116
+ surfaces.forEach(surface => {
117
+ surface.classList.add('eos-notification-safe');
118
+ surface.style.pointerEvents = 'auto';
119
+ surface.querySelectorAll('button,[role="button"],a,.MuiIconButton-root').forEach(restoreCloseButton);
120
+ });
121
+ });
122
+
123
+ const run = root => {
124
+ repairSecurityText(root);
125
+ repairNotifications(root);
126
+ };
127
+
128
+ const install = () => {
129
+ run(document.body || document.documentElement);
130
+ const observer = new MutationObserver(mutations => {
131
+ const roots = new Set();
132
+ for (const mutation of mutations) {
133
+ if (mutation.type === 'characterData') roots.add(mutation.target.parentElement || document.body);
134
+ mutation.addedNodes?.forEach(node => roots.add(node));
135
+ }
136
+ roots.forEach(root => run(root));
137
+ });
138
+ observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true });
139
+ [250, 1000, 3000, 8000].forEach(ms => window.setTimeout(() => run(document.body || document.documentElement), ms));
140
+ };
141
+
142
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', install, { once: true });
143
+ else install();
144
+ })();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v31-security-text-polish';
4
+ const VERSION = 'v38-popup-safe-security-polish';
5
5
  const LEGACY_ADMIN = 'admin';
6
6
  const LEGACY_ADMIN_INSTANCE = 'admin.0';
7
7
  const ASSET_BASE = (() => {
@@ -34,14 +34,24 @@
34
34
  let text = String(value || '');
35
35
  const map = new Map(Object.entries({
36
36
  'dürfen': 'dürfen', 'Dürfen': 'Dürfen', 'für': 'für', 'Für': 'Für',
37
- 'können': 'können', 'Können': 'Können', 'möglich': 'möglich', 'Möglich': 'Möglich',
38
- 'Löschen': 'Löschen', 'löschen': 'löschen', 'schützen': 'schützen', 'Schützen': 'Schützen',
39
- 'Schützt': 'Schützt', 'schützt': 'schützt', 'Geschützte': 'Geschützte', 'geschützte': 'geschützte',
37
+ 'müssen': 'müssen', 'Müssen': 'Müssen', 'können': 'können', 'Können': 'Können',
38
+ 'möglich': 'möglich', 'Möglich': 'Möglich', 'Löschen': 'Löschen', 'löschen': 'löschen',
39
+ 'schützen': 'schützen', 'Schützen': 'Schützen', 'Schützt': 'Schützt', 'schützt': 'schützt',
40
+ 'Geschützte': 'Geschützte', 'geschützte': 'geschützte', 'Geschützter': 'Geschützter',
40
41
  'ausgewählte': 'ausgewählte', 'Ausgewählte': 'Ausgewählte', 'ändern': 'ändern', 'Ändern': 'Ändern',
41
42
  'über': 'über', 'Über': 'Über', 'Wähle': 'Wähle', 'wähle': 'wähle',
42
- 'ß': 'ß', 'Ä': 'Ä', 'Ö': 'Ö', 'Ü': 'Ü', 'ä': 'ä', 'ö': 'ö', 'ü': 'ü'
43
+ 'öffnen': 'öffnen', 'Öffnen': 'Öffnen', 'schließen': 'schließen', 'Schließen': 'Schließen',
44
+ 'Gerät': 'Gerät', 'Geräte': 'Geräte', 'Geräteliste': 'Geräteliste',
45
+ 'ß': 'ß', 'Ä': 'Ä', 'Ö': 'Ö', 'Ü': 'Ü', 'ä': 'ä', 'ö': 'ö', 'ü': 'ü', 'Â': ''
43
46
  }));
44
47
  for (const [from, to] of map) if (text.includes(from)) text = text.split(from).join(to);
48
+ // Generic Latin1-as-UTF8 repair for labels injected by older bundles. Guard against false positives.
49
+ if (/[ÃÂ]/.test(text)) {
50
+ try {
51
+ const repaired = decodeURIComponent(escape(text));
52
+ if (/[äöüÄÖÜß]/.test(repaired) && !/[ÃÂ]/.test(repaired)) text = repaired;
53
+ } catch { /* keep mapped text */ }
54
+ }
45
55
  return text;
46
56
  };
47
57
  const normalizeFlat = value => normalize(value).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
@@ -220,12 +230,14 @@
220
230
  };
221
231
 
222
232
  const applyPolicyToDom = () => {
223
- if (isAdapterConfigSurface()) return;
233
+ // Text repair is safe and must also run on adapter config pages.
234
+ replaceTextNodes();
224
235
  const admin = isAdminUser();
225
236
  document.documentElement.classList.toggle('eos-security-admin-user', admin);
226
237
  document.documentElement.classList.toggle('eos-security-non-admin-user', !admin);
227
238
  document.documentElement.classList.toggle('eos-security-nonadmin', !admin);
228
- replaceTextNodes();
239
+ releaseNotificationControls();
240
+ if (isAdapterConfigSurface()) return;
229
241
  hideLegacyAdminPanels();
230
242
  hideProtectedDeleteControls();
231
243
  hideEosSecuritySettingsForNonAdmins();
@@ -233,6 +245,25 @@
233
245
 
234
246
  const isAdapterConfigSurface = () => document.documentElement.classList.contains('eos-adapter-config-surface') || /Instanzeinstellungen:|Instance settings:|Geräteliste|Gerät hinzufügen|Gerät bearbeiten/i.test(document.body?.textContent || '');
235
247
 
248
+ const releaseNotificationControls = () => {
249
+ // v38: security UI must not modify generic dialogs or adapter popups.
250
+ // Only snackbar/toast surfaces are normalized for clickability.
251
+ const roots = [
252
+ '.MuiSnackbar-root', '.SnackbarItem-root', '.SnackbarItem-wrappedRoot',
253
+ '.notistack-Snackbar', '.Toastify__toast-container', '.Toastify__toast'
254
+ ].join(',');
255
+ document.querySelectorAll(roots).forEach(box => {
256
+ if (box.closest('.MuiDialog-root,.MuiModal-root,.MuiPopover-root,.MuiPopper-root,.MuiMenu-root,[role="dialog"]')) return;
257
+ box.classList.add('eos-notification-safe');
258
+ box.style.pointerEvents = 'auto';
259
+ box.querySelectorAll('button, [role="button"], a, .MuiIconButton-root').forEach(control => {
260
+ control.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete');
261
+ control.style.pointerEvents = 'auto';
262
+ control.style.visibility = '';
263
+ });
264
+ });
265
+ };
266
+
236
267
  const scheduleApply = () => {
237
268
  if (state.scheduled) return;
238
269
  state.scheduled = true;
@@ -271,6 +302,8 @@
271
302
  document.addEventListener('click', event => {
272
303
  const target = event.target?.closest?.('button,[role="button"],a,[role="menuitem"],.MuiMenuItem-root');
273
304
  if (!target || isAdminUser() || isAdapterConfigSurface()) return;
305
+ // Native Admin dialogs, install/autocomplete poppers and adapter-owned menus must stay untouched.
306
+ if (target.closest?.('.MuiDialog-root,.MuiModal-root,.MuiPopover-root,.MuiPopper-root,.MuiMenu-root,.MuiAutocomplete-popper,[role="dialog"],[role="listbox"],[role="menu"]')) return;
274
307
  const label = normalizeFlat(`${target.textContent || ''} ${target.getAttribute?.('title') || ''} ${target.getAttribute?.('aria-label') || ''}`);
275
308
  if (/loschen|delete|remove|deinstall|uninstall/.test(label)) {
276
309
  target.classList.add('eos-security-hidden-delete');
@@ -25,9 +25,9 @@
25
25
  "Password is too short (min 6 chars)": "Das Passwort ist zu kurz (mindestens 6 Zeichen)",
26
26
  "Password repeat": "Passwort wiederholen",
27
27
  "Password successfully changed for \"%s\"": "Passwort für „%s“ erfolgreich geändert",
28
- "Schützt ausgewählte Adapter für Installateur-/Endkundenbenutzer über Administrator-ACLs und EOS-UI-Regeln. dontDelete und nondeletable bleiben false, damit Updates weiter funktionieren.": "Schützt ausgewählte Adapter für Installateur- und Endkundenbenutzer über Administratorrechte und EOS-Regeln. dontDelete und nondeletable bleiben false, damit Updates weiter funktionieren.",
28
+ "Schützt ausgewählte Adapter in der EOS Oberfläche vor kritischen Aktionen durch Installateur- und Endkundenrollen. Adaptereigene Funktionen und Updates bleiben möglich.": "Schützt ausgewählte Adapter für Installateur- und Endkundenbenutzer über Administratorrechte und EOS-Regeln. dontDelete und nondeletable bleiben false, damit Updates weiter funktionieren.",
29
29
  "Set password": "Passwort festlegen",
30
- "Setzt Administrator-ACLs auf die geschützte Adapterliste. Installateur-/Endkundenbenutzer können diese Adapter nicht stoppen, aktivieren oder löschen; Administratoren können sie weiterhin updaten und warten.": "Setzt Administratorrechte auf die geschützte Adapterliste. Installateur- und Endkundenbenutzer können diese Adapter nicht stoppen, aktivieren oder löschen. Administratoren können sie weiterhin updaten und warten.",
30
+ "Blendet kritische Aktionen für Installateur- und Endkundenrollen aus. Harte ACL-Sperren werden nicht auf Runtime-Adapter gesetzt, damit BackItUp und adaptereigene Konfigurationen stabil bleiben.": "Setzt Administratorrechte auf die geschützte Adapterliste. Installateur- und Endkundenbenutzer können diese Adapter nicht stoppen, aktivieren oder löschen. Administratoren können sie weiterhin updaten und warten.",
31
31
  "Sperr-Adresse des alten Admins": "Sperr-Adresse des alten Admins",
32
32
  "Sperr-Port des alten Admins": "Sperr-Port des alten Admins",
33
33
  "The password for user \"%s\" was successfully changed": "Das Passwort für den Benutzer „%s“ wurde erfolgreich geändert",