iobroker.eos-admin 7.9.36 → 7.9.37

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 v37: 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,101 @@ 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
+ /* === NexoWatt EOS v37: notification close compatibility ==================
3376
+ Toasts/snackbars/alerts must stay above EOS decoration and their close
3377
+ actions must remain clickable on all routes. */
3378
+ html.eos-app .MuiSnackbar-root,
3379
+ html.eos-app .SnackbarItem-root,
3380
+ html.eos-app .SnackbarItem-wrappedRoot,
3381
+ html.eos-app .notistack-Snackbar,
3382
+ html.eos-app .Toastify__toast-container,
3383
+ html.eos-app .Toastify__toast,
3384
+ html.eos-app [role="alert"],
3385
+ html.eos-app .MuiAlert-root {
3386
+ pointer-events: auto !important;
3387
+ z-index: 5200 !important;
3388
+ }
3389
+ html.eos-app .MuiSnackbar-root button,
3390
+ html.eos-app .SnackbarItem-root button,
3391
+ html.eos-app .notistack-Snackbar button,
3392
+ html.eos-app .Toastify__toast button,
3393
+ html.eos-app .MuiAlert-root button,
3394
+ html.eos-app [role="alert"] button,
3395
+ html.eos-app button[aria-label="close"],
3396
+ html.eos-app button[aria-label="Close"],
3397
+ html.eos-app button[title="Schließen"],
3398
+ html.eos-app button[title="Close"] {
3399
+ pointer-events: auto !important;
3400
+ opacity: 1 !important;
3401
+ visibility: visible !important;
3402
+ z-index: 5201 !important;
3403
+ }
3404
+ html.eos-app .MuiSnackbar-root svg,
3405
+ html.eos-app .SnackbarItem-root svg,
3406
+ html.eos-app .MuiAlert-root svg,
3407
+ html.eos-app [role="alert"] svg {
3408
+ pointer-events: none !important;
3409
+ }
3410
+
3411
+ /* v37: BackItUp-safe mode. EOS keeps delete/stop controls protected in the UI,
3412
+ but does not force adapter-object ACLs for runtime adapters by default. */
3413
+ html.eos-app .eos-backitup-safe-note { color: rgba(226,245,255,.78); }
3414
+
3415
+ /* v37: native Admin notifications must always be closable. */
3416
+ html.eos-app .MuiSnackbar-root,
3417
+ html.eos-app .MuiAlert-root,
3418
+ html.eos-app .MuiSnackbarContent-root,
3419
+ html.eos-app [role="alert"],
3420
+ html.eos-app .Toastify__toast,
3421
+ html.eos-app .notistack-Snackbar,
3422
+ html.eos-app .eos-notification-safe {
3423
+ pointer-events: auto !important;
3424
+ z-index: 5200 !important;
3425
+ }
3426
+ html.eos-app .MuiSnackbar-root button,
3427
+ html.eos-app .MuiAlert-root button,
3428
+ html.eos-app .MuiSnackbarContent-root button,
3429
+ html.eos-app [role="alert"] button,
3430
+ html.eos-app .Toastify__toast button,
3431
+ html.eos-app .notistack-Snackbar button,
3432
+ html.eos-app .eos-notification-safe button,
3433
+ html.eos-app .eos-notification-safe [role="button"],
3434
+ html.eos-app .eos-notification-safe a,
3435
+ html.eos-app .eos-notification-safe .MuiIconButton-root {
3436
+ pointer-events: auto !important;
3437
+ visibility: visible !important;
3438
+ opacity: 1 !important;
3439
+ }
3440
+ html.eos-app .eos-notification-safe .eos-protected-delete-control,
3441
+ html.eos-app .eos-notification-safe .eos-security-hidden-delete {
3442
+ display: inline-flex !important;
3443
+ pointer-events: auto !important;
3444
+ }
3445
+
3446
+
3447
+ /* v37 notification dialog safety: notification dialogs must stay fully native/clickable. */
3448
+ html.eos-app .eos-notification-dialog-root,
3449
+ html.eos-app .MuiModal-root:has(#notifications-dialog-close),
3450
+ html.eos-app .MuiDialog-root:has(#notifications-dialog-close) {
3451
+ pointer-events: auto !important;
3452
+ z-index: 4300 !important;
3453
+ }
3454
+ html.eos-app .eos-notification-dialog,
3455
+ html.eos-app .MuiDialog-paper:has(#notifications-dialog-close) {
3456
+ pointer-events: auto !important;
3457
+ z-index: 4301 !important;
3458
+ }
3459
+ html.eos-app .eos-notification-dialog button,
3460
+ html.eos-app .eos-notification-dialog [role="button"],
3461
+ html.eos-app .MuiDialog-paper:has(#notifications-dialog-close) button,
3462
+ html.eos-app .MuiDialog-paper:has(#notifications-dialog-close) [role="button"] {
3463
+ pointer-events: auto !important;
3464
+ visibility: visible !important;
3465
+ }
3466
+
3467
+ /* v37: security text must never show mojibake-like fragments after runtime repair. */
3468
+ html.eos-app .eos-security-admin-only-field,
3469
+ html.eos-app .eos-settings-dialog {
3470
+ unicode-bidi: plaintext;
3471
+ }
@@ -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=37" />
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=37"></script>
158
+ <script defer src="./js/eos-security-ui.js?v=37"></script>
159
+ <script defer src="./js/eos-assistant.js?v=37"></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 = 'v37-notification-backitup-security-text-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,26 @@
335
403
  });
336
404
  };
337
405
 
406
+
407
+ const releaseNotificationControls = () => safe(() => {
408
+ // v37: notification/snackbar close buttons belong to the native Admin UI.
409
+ // They must never be disabled or covered by EOS security/layout layers.
410
+ document.querySelectorAll('.MuiSnackbar-root, .MuiAlert-root, .MuiSnackbarContent-root, [role="alert"], .Toastify__toast, .notistack-Snackbar').forEach(box => {
411
+ box.classList.add('eos-notification-safe');
412
+ box.style.pointerEvents = 'auto';
413
+ box.querySelectorAll('button, [role="button"], a, .MuiIconButton-root, svg').forEach(control => {
414
+ control.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete');
415
+ control.removeAttribute('disabled');
416
+ control.removeAttribute('aria-disabled');
417
+ if ('disabled' in control) control.disabled = false;
418
+ control.style.pointerEvents = 'auto';
419
+ control.style.display = '';
420
+ control.style.visibility = '';
421
+ control.style.opacity = '';
422
+ });
423
+ });
424
+ });
425
+
338
426
  const protectDeleteDialogs = () => {
339
427
  if (isAdminUser() || state.securityPolicy.restrictProtectedAdapterControls === false) return;
340
428
  const protectedAdapters = state.securityPolicy.protectedAdapters || [];
@@ -398,6 +486,7 @@
398
486
  const applySecurityUiGuard = () => safe(() => {
399
487
  const policy = state.securityPolicy;
400
488
  applySecurityClasses();
489
+ releaseNotificationControls();
401
490
  // Do not apply EOS security decoration inside native adapter configuration pages.
402
491
  // Adapter UIs must remain 100% functional; backend/role checks still protect EOS actions.
403
492
  if (isAdapterConfigSurface()) return;
@@ -747,6 +836,7 @@
747
836
  }
748
837
  patchDrawerHeader(document.querySelector('.MuiDrawer-paper'));
749
838
  hideNativeLogoutNav();
839
+ patchNotifications();
750
840
  removeLogoutButton();
751
841
  });
752
842
 
@@ -867,6 +957,26 @@
867
957
  });
868
958
 
869
959
 
960
+
961
+
962
+ const ensureNotificationDialogClasses = () => safe(() => {
963
+ document.querySelectorAll('.MuiDialog-root, .MuiModal-root, [role="presentation"]').forEach(root => {
964
+ const paper = root.querySelector?.('.MuiDialog-paper, [role="dialog"]');
965
+ if (!paper) return;
966
+ const txt = normalize(paper.textContent || '');
967
+ if (!/(benachrichtigungen|notifications|acknowledge|bestätigen|schließen|close)/i.test(txt) && !paper.querySelector('#notifications-dialog-close')) return;
968
+ root.classList.add('eos-notification-dialog-root');
969
+ paper.classList.add('eos-notification-dialog');
970
+ paper.querySelectorAll('button, [role="button"], a, .MuiButtonBase-root, .MuiIconButton-root').forEach(control => {
971
+ control.style.pointerEvents = 'auto';
972
+ control.style.userSelect = 'auto';
973
+ if (control.getAttribute('aria-disabled') === 'true' && /schließen|close/i.test(control.textContent || control.getAttribute('aria-label') || control.getAttribute('title') || '')) {
974
+ control.removeAttribute('aria-disabled');
975
+ }
976
+ });
977
+ });
978
+ });
979
+
870
980
  const ensureSettingsDialogClasses = () => safe(() => {
871
981
  const dialogs = Array.from(document.querySelectorAll('.MuiDialog-paper, [role="dialog"]'));
872
982
  dialogs.forEach(dialog => {
@@ -920,6 +1030,28 @@
920
1030
  });
921
1031
 
922
1032
 
1033
+
1034
+
1035
+ const patchNotifications = () => safe(() => {
1036
+ const selectors = [
1037
+ '.MuiSnackbar-root', '.SnackbarItem-root', '.SnackbarItem-wrappedRoot', '.notistack-Snackbar',
1038
+ '.Toastify__toast-container', '.Toastify__toast', '.MuiAlert-root', '[role="alert"]'
1039
+ ];
1040
+ document.querySelectorAll(selectors.join(',')).forEach(node => {
1041
+ node.classList.add('eos-notification-surface');
1042
+ if (node.style) {
1043
+ node.style.pointerEvents = 'auto';
1044
+ if (!node.closest('.MuiDialog-root')) node.style.zIndex = '5200';
1045
+ }
1046
+ node.querySelectorAll('button,[role="button"],a').forEach(control => {
1047
+ control.classList.add('eos-notification-action');
1048
+ control.style.pointerEvents = 'auto';
1049
+ control.style.visibility = 'visible';
1050
+ control.style.opacity = '1';
1051
+ });
1052
+ });
1053
+ });
1054
+
923
1055
  const applyNavCompactPreference = () => safe(() => {
924
1056
  const compact = localStorage.getItem('nexowatt:eosNavCompact') === '1';
925
1057
  document.documentElement.classList.toggle('eos-nav-compact', compact);
@@ -1223,10 +1355,17 @@
1223
1355
  ensureRightsHelper();
1224
1356
  ensurePermissionPresets();
1225
1357
  ensureSettingsDialogClasses();
1358
+ ensureNotificationDialogClasses();
1226
1359
  hideNativeLogoutNav();
1227
1360
  hideOfficialNexoWattRepoWarning();
1361
+ patchNotifications();
1228
1362
  applySecurityUiGuard();
1229
1363
  if (isAdapterConfigSurface()) {
1364
+ // Adapter-owned configuration pages must not be rebranded or structurally patched.
1365
+ // We still repair broken UTF-8/mojibake text because jsonConfig labels can be
1366
+ // rendered through different legacy paths. This is text-only and does not touch
1367
+ // adapter controls, React state, events or attributes.
1368
+ patchMojibakeTextNodes(document.getElementById('app-paper'));
1230
1369
  ['.MuiAppBar-root', '.MuiDrawer-paper', 'nav', '.eos-brand-badge', '.eos-top-toolbar'].forEach(selector => {
1231
1370
  document.querySelectorAll(selector).forEach(scope => {
1232
1371
  patchTextNodes(scope);
@@ -1255,12 +1394,17 @@
1255
1394
  ensureRightsHelper();
1256
1395
  ensurePermissionPresets();
1257
1396
  ensureSettingsDialogClasses();
1397
+ ensureNotificationDialogClasses();
1258
1398
  hideNativeLogoutNav();
1259
1399
  hideOfficialNexoWattRepoWarning();
1400
+ patchNotifications();
1260
1401
  applySecurityUiGuard();
1261
1402
  for (const scope of scopes.slice(0, 80)) {
1262
1403
  if (!scope || !scope.isConnected) continue;
1263
- if (isAdapterConfigSurface() && (scope.id === 'app-paper' || scope.closest?.('#app-paper'))) continue;
1404
+ if (isAdapterConfigSurface() && (scope.id === 'app-paper' || scope.closest?.('#app-paper'))) {
1405
+ patchMojibakeTextNodes(scope);
1406
+ continue;
1407
+ }
1264
1408
  patchTextNodes(scope);
1265
1409
  patchAttributes(scope);
1266
1410
  }
@@ -1291,14 +1435,17 @@
1291
1435
  const observer = new MutationObserver(mutations => {
1292
1436
  for (const mutation of mutations) {
1293
1437
  if (mutation.type === 'characterData') {
1294
- patchTextNode(mutation.target);
1438
+ if (isAdapterConfigSurface() && mutation.target?.parentElement?.closest?.('#app-paper')) patchMojibakeTextNode(mutation.target);
1439
+ else patchTextNode(mutation.target);
1295
1440
  continue;
1296
1441
  }
1297
1442
  if (mutation.type !== 'childList') continue;
1298
1443
  mutation.addedNodes.forEach(node => {
1299
1444
  if (!node) return;
1300
- if (node.nodeType === Node.TEXT_NODE) patchTextNode(node);
1301
- else if (node.nodeType === Node.ELEMENT_NODE) state.pendingScopes.add(node);
1445
+ if (node.nodeType === Node.TEXT_NODE) {
1446
+ if (isAdapterConfigSurface() && node.parentElement?.closest?.('#app-paper')) patchMojibakeTextNode(node);
1447
+ else patchTextNode(node);
1448
+ } else if (node.nodeType === Node.ELEMENT_NODE) state.pendingScopes.add(node);
1302
1449
  });
1303
1450
  }
1304
1451
  if (state.pendingScopes.size) scheduleScopePatch();
@@ -1327,3 +1474,21 @@
1327
1474
  window.addEventListener('load', () => scheduleFullPatch(0), { once: true });
1328
1475
  window.addEventListener('hashchange', () => scheduleFullPatch(0));
1329
1476
  })();
1477
+
1478
+
1479
+ // v37 eos notification close compatibility: never let EOS overlays block native notification dialogs.
1480
+ (() => {
1481
+ const normalize = value => String(value || '').replace(/\s+/g, ' ').trim();
1482
+ document.addEventListener('click', event => {
1483
+ const target = event.target?.closest?.('button, [role="button"], a, .MuiButtonBase-root, .MuiIconButton-root');
1484
+ if (!target) return;
1485
+ const dialog = target.closest?.('.eos-notification-dialog, .MuiDialog-paper, [role="dialog"]');
1486
+ if (!dialog || !/benachrichtigungen|notifications|acknowledge|bestätigen|schließen|close/i.test(dialog.textContent || '')) return;
1487
+ const label = normalize(`${target.textContent || ''} ${target.getAttribute?.('aria-label') || ''} ${target.getAttribute?.('title') || ''}`);
1488
+ if (/schließen|close|bestätigen|acknowledge/i.test(label)) {
1489
+ target.style.pointerEvents = 'auto';
1490
+ // Do not prevent React handlers; only stop EOS-specific bubbling side effects.
1491
+ event.stopPropagation();
1492
+ }
1493
+ }, true);
1494
+ })();
@@ -0,0 +1,144 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ window.NEXOWATT_EOS_RUNTIME_FIXES_VERSION = 'v37-notification-backitup-security-text-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
+ '.MuiAlert-root',
82
+ '[role="alert"]',
83
+ '[role="status"]',
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
+ '.MuiAlert-root',
111
+ '[role="alert"]',
112
+ '[role="status"]',
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 = 'v37-security-text-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,24 @@
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
+ // Never block notification/toast close actions. These controls are owned by
250
+ // the native Admin UI and must remain clickable regardless of EOS security rules.
251
+ document.querySelectorAll('.MuiSnackbar-root, .MuiAlert-root, .MuiSnackbarContent-root, [role="alert"], .Toastify__toast, .notistack-Snackbar').forEach(box => {
252
+ box.classList.add('eos-notification-safe');
253
+ box.style.pointerEvents = 'auto';
254
+ box.querySelectorAll('button, [role="button"], a, .MuiIconButton-root').forEach(control => {
255
+ control.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete');
256
+ control.removeAttribute('disabled');
257
+ control.removeAttribute('aria-disabled');
258
+ if ('disabled' in control) control.disabled = false;
259
+ control.style.pointerEvents = 'auto';
260
+ control.style.display = '';
261
+ control.style.visibility = '';
262
+ });
263
+ });
264
+ };
265
+
236
266
  const scheduleApply = () => {
237
267
  if (state.scheduled) return;
238
268
  state.scheduled = true;
@@ -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",