iobroker.eos-admin 7.9.23 → 7.9.25

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.
@@ -2203,3 +2203,84 @@ html.eos-app .eos-hidden-logout * {
2203
2203
  .eos-hidden-nexowatt-repo-warning {
2204
2204
  display: none !important;
2205
2205
  }
2206
+
2207
+
2208
+ /* NexoWatt EOS security visibility */
2209
+ .eos-hidden-legacy-admin {
2210
+ display: none !important;
2211
+ }
2212
+
2213
+ .eos-hide-legacy-admin-active [data-id="admin"],
2214
+ .eos-hide-legacy-admin-active [data-id="admin.0"],
2215
+ .eos-hide-legacy-admin-active [data-name="admin"] {
2216
+ display: none !important;
2217
+ }
2218
+
2219
+ /* v25: role-aware EOS security UI guard */
2220
+ html.eos-security-nonadmin .eos-hidden-legacy-admin {
2221
+ display: none !important;
2222
+ }
2223
+
2224
+ html.eos-security-nonadmin .eos-protected-adapter-row .eos-protected-delete-control,
2225
+ html.eos-security-nonadmin .eos-protected-delete-control {
2226
+ opacity: 0.28 !important;
2227
+ pointer-events: none !important;
2228
+ filter: grayscale(1) !important;
2229
+ }
2230
+
2231
+ html.eos-security-nonadmin .eos-protected-delete-dialog .eos-protected-delete-control {
2232
+ display: none !important;
2233
+ }
2234
+
2235
+ html.eos-security-nonadmin .eos-security-admin-only-field {
2236
+ display: none !important;
2237
+ }
2238
+
2239
+ .eos-security-restricted-note {
2240
+ margin: 10px 0 18px !important;
2241
+ padding: 14px 16px !important;
2242
+ border-radius: 16px !important;
2243
+ border: 1px solid rgba(45, 255, 182, 0.26) !important;
2244
+ background: rgba(4, 18, 28, 0.78) !important;
2245
+ color: rgba(232, 255, 248, 0.95) !important;
2246
+ box-shadow: 0 0 22px rgba(0, 229, 168, 0.13) !important;
2247
+ display: flex !important;
2248
+ flex-direction: column !important;
2249
+ gap: 4px !important;
2250
+ }
2251
+
2252
+ .eos-security-restricted-note strong {
2253
+ color: #53ffd0 !important;
2254
+ font-weight: 800 !important;
2255
+ }
2256
+
2257
+ .eos-security-restricted-note span {
2258
+ color: rgba(219, 246, 240, 0.78) !important;
2259
+ font-size: 0.9rem !important;
2260
+ }
2261
+
2262
+
2263
+ /* NexoWatt EOS v25 security visibility guard */
2264
+ .eos-security-hidden,
2265
+ .eos-security-hidden-delete {
2266
+ display: none !important;
2267
+ }
2268
+ .eos-security-protected-adapter {
2269
+ position: relative;
2270
+ }
2271
+ .eos-security-protected-adapter::after {
2272
+ content: 'EOS geschützt';
2273
+ position: absolute;
2274
+ right: 10px;
2275
+ top: 10px;
2276
+ z-index: 2;
2277
+ font-size: 10px;
2278
+ letter-spacing: .06em;
2279
+ text-transform: uppercase;
2280
+ padding: 3px 8px;
2281
+ border-radius: 999px;
2282
+ color: rgba(225, 255, 246, .88);
2283
+ background: rgba(5, 22, 28, .72);
2284
+ border: 1px solid rgba(55, 230, 180, .22);
2285
+ pointer-events: none;
2286
+ }
@@ -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=23" />
34
+ <link rel="stylesheet" href="./css/eos-branding.css?v=25" />
35
35
  <link
36
36
  rel="manifest"
37
37
  href="manifest.json"
@@ -154,7 +154,8 @@
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=23"></script>
157
+ <script defer src="./js/eos-branding.js?v=25"></script>
158
+ <script defer src="./js/eos-security-ui.js?v=25"></script>
158
159
  </head>
159
160
  <body>
160
161
  <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 = 'v23-official-nexowatt-repo-warning-fix';
4
+ window.NEXOWATT_EOS_UI_VERSION = 'v25-admin-visibility-acl-guard';
5
5
 
6
6
  const BRAND = 'NexoWatt EOS';
7
7
  const EOS_MEANING = 'Energy Operation System';
@@ -64,6 +64,14 @@
64
64
  scopePatchScheduled: false,
65
65
  pendingScopes: new Set(),
66
66
  lastFullPatch: 0,
67
+ securityPolicy: {
68
+ loaded: false,
69
+ isAdmin: false,
70
+ hideLegacyAdminForNonAdmins: true,
71
+ restrictProtectedAdapterControls: true,
72
+ protectedAdapters: ['eos-admin', 'backitup'],
73
+ },
74
+ securityFetchStarted: false,
67
75
  };
68
76
 
69
77
  const safe = fn => {
@@ -163,6 +171,214 @@
163
171
  };
164
172
  };
165
173
 
174
+ const normalizeIdentifier = value => String(value || '')
175
+ .toLowerCase()
176
+ .normalize('NFD')
177
+ .replace(/[\u0300-\u036f]/g, '')
178
+ .replace(/^iobroker\./, '')
179
+ .replace(/^system\.adapter\./, '')
180
+ .replace(/\.\d+$/, '')
181
+ .replace(/[^a-z0-9_.-]+/g, ' ')
182
+ .trim();
183
+
184
+ const adapterPattern = adapter => {
185
+ const escaped = String(adapter || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
186
+ return new RegExp(`(?:^|[^a-z0-9_-])${escaped}(?:$|[^a-z0-9_-])`, 'i');
187
+ };
188
+
189
+ const textOfElement = el => {
190
+ if (!el) return '';
191
+ const attrs = ['data-adapter-name', 'data-adapter', 'data-id', 'id', 'title', 'aria-label', 'alt', 'href', 'src'];
192
+ const values = [el.textContent || ''];
193
+ attrs.forEach(attr => {
194
+ if (el.getAttribute && el.hasAttribute(attr)) values.push(el.getAttribute(attr) || '');
195
+ });
196
+ return values.join(' ');
197
+ };
198
+
199
+ const securityEndpointUrls = () => [
200
+ new URL('nexowatt/security/context', ASSET_BASE).href,
201
+ new URL('nexowatt/security/session', ASSET_BASE).href,
202
+ new URL('eos/security/status', ASSET_BASE).href,
203
+ ];
204
+
205
+ const normalizeSecurityPolicy = policy => {
206
+ const protectedAdapters = new Set(['eos-admin', 'backitup']);
207
+ (Array.isArray(policy?.protectedAdapters) ? policy.protectedAdapters : []).forEach(item => {
208
+ const adapter = typeof item === 'string' ? normalizeIdentifier(item) : normalizeIdentifier(item?.adapter || item?.name);
209
+ if (adapter) protectedAdapters.add(adapter);
210
+ });
211
+ const isAdmin = !!(policy?.isAdmin || policy?.isAdminGroup || policy?.isEosAdminGroup || policy?.isAdministrator);
212
+ return {
213
+ loaded: true,
214
+ user: policy?.user || null,
215
+ groups: Array.isArray(policy?.groups) ? policy.groups : [],
216
+ isAdmin,
217
+ hideLegacyAdminForNonAdmins: policy?.hideLegacyAdminForNonAdmins !== false && policy?.hideLegacyAdminFromNonAdmins !== false,
218
+ restrictProtectedAdapterControls: policy?.restrictProtectedAdapterControls !== false,
219
+ protectedAdapters: [...protectedAdapters].sort(),
220
+ };
221
+ };
222
+
223
+ const applySecurityClasses = () => {
224
+ const policy = state.securityPolicy;
225
+ document.documentElement.classList.toggle('eos-security-loaded', !!policy.loaded);
226
+ document.documentElement.classList.toggle('eos-security-admin', !!policy.isAdmin);
227
+ document.documentElement.classList.toggle('eos-security-nonadmin', policy.loaded && !policy.isAdmin);
228
+ };
229
+
230
+ const fetchSecurityPolicy = async () => {
231
+ if (state.securityFetchStarted) return;
232
+ state.securityFetchStarted = true;
233
+ for (const url of securityEndpointUrls()) {
234
+ try {
235
+ const response = await fetch(url, { credentials: 'same-origin', cache: 'no-store' });
236
+ if (!response.ok) continue;
237
+ const json = await response.json();
238
+ if (!json || json.error) continue;
239
+ state.securityPolicy = normalizeSecurityPolicy(json);
240
+ applySecurityClasses();
241
+ scheduleFullPatch(0);
242
+ return;
243
+ } catch (e) {
244
+ // Security endpoint may be unavailable during login or old cache. Fallback below.
245
+ }
246
+ }
247
+ state.securityPolicy = normalizeSecurityPolicy({ isAdmin: false, protectedAdapters: ['eos-admin', 'backitup'] });
248
+ applySecurityClasses();
249
+ scheduleFullPatch(0);
250
+ };
251
+
252
+ const isAdminUser = () => !!state.securityPolicy?.isAdmin;
253
+
254
+ const isLegacyAdminContainer = el => {
255
+ const text = textOfElement(el);
256
+ if (/\badmin\.0\b/i.test(text) || /\bsystem\.adapter\.admin\.0\b/i.test(text)) return true;
257
+ const candidates = Array.from(el.querySelectorAll ? el.querySelectorAll('*') : []);
258
+ candidates.push(el);
259
+ return candidates.some(node => {
260
+ const value = textOfElement(node).trim();
261
+ return /^admin$/i.test(value) || /^iobroker\.admin$/i.test(value) || /^system\.adapter\.admin$/i.test(value);
262
+ });
263
+ };
264
+
265
+ const containerMentionsAdapter = (container, adapter) => {
266
+ if (!container || !adapter) return false;
267
+ const normalized = normalizeIdentifier(adapter);
268
+ if (!normalized) return false;
269
+ if (normalized === 'admin') return isLegacyAdminContainer(container);
270
+ const pattern = adapterPattern(normalized);
271
+ if (pattern.test(textOfElement(container))) return true;
272
+ return Array.from(container.querySelectorAll ? container.querySelectorAll('[title],[aria-label],[data-adapter-name],[data-adapter],[data-id]') : [])
273
+ .some(el => pattern.test(textOfElement(el)));
274
+ };
275
+
276
+ const getSecurityContainers = () => Array.from(document.querySelectorAll([
277
+ '#app-paper .MuiCard-root',
278
+ '#app-paper tr.MuiTableRow-root',
279
+ '#app-paper tr',
280
+ '#app-paper .MuiAccordion-root',
281
+ '#app-paper [role="row"]',
282
+ '#app-paper [role="treeitem"]',
283
+ '#app-paper .MuiDataGrid-row',
284
+ '#app-paper .MuiTreeItem-root',
285
+ '#app-paper .MuiFormControlLabel-root',
286
+ '#app-paper .MuiListItem-root',
287
+ '#app-paper .MuiListItemButton-root',
288
+ ].join(','))).filter(el => !el.closest('.MuiDialog-paper'));
289
+
290
+ const isDeleteControl = el => {
291
+ const text = normalize(el.textContent || el.getAttribute?.('title') || el.getAttribute?.('aria-label') || '');
292
+ const title = normalize(el.getAttribute?.('title') || el.closest?.('[title]')?.getAttribute('title') || '');
293
+ const svg = el.querySelector?.('svg[data-testid*="Delete"], svg[data-testid*="Remove"], svg[data-testid*="Clear"]');
294
+ return !!svg || /\b(delete|remove|uninstall|del|loschen|entfernen|deinstallieren)\b/.test(`${text} ${title}`);
295
+ };
296
+
297
+ const lockDeleteControls = container => {
298
+ Array.from(container.querySelectorAll('button, [role="button"], a')).forEach(control => {
299
+ if (!isDeleteControl(control)) return;
300
+ control.classList.add('eos-protected-delete-control');
301
+ control.setAttribute('aria-disabled', 'true');
302
+ control.setAttribute('title', 'Nur Administratoren dürfen geschützte EOS-Systemmodule löschen');
303
+ if ('disabled' in control) control.disabled = true;
304
+ control.addEventListener('click', event => {
305
+ event.preventDefault();
306
+ event.stopImmediatePropagation();
307
+ }, true);
308
+ });
309
+ };
310
+
311
+ const protectDeleteDialogs = () => {
312
+ if (isAdminUser() || state.securityPolicy.restrictProtectedAdapterControls === false) return;
313
+ const protectedAdapters = state.securityPolicy.protectedAdapters || [];
314
+ Array.from(document.querySelectorAll('.MuiDialog-paper, [role="dialog"]')).forEach(dialog => {
315
+ const text = textOfElement(dialog);
316
+ if (!/(delete|remove|loschen|entfernen|deinstallieren|del\s+)/i.test(text)) return;
317
+ const protectedHit = protectedAdapters.some(adapter => containerMentionsAdapter(dialog, adapter));
318
+ if (!protectedHit) return;
319
+ dialog.classList.add('eos-protected-delete-dialog');
320
+ Array.from(dialog.querySelectorAll('button')).forEach(button => {
321
+ const label = normalize(button.textContent || button.getAttribute('aria-label') || '');
322
+ if (/^(ok|ja|yes|delete|remove|loschen|entfernen|deinstallieren)$/.test(label)) {
323
+ button.disabled = true;
324
+ button.classList.add('eos-protected-delete-control');
325
+ }
326
+ });
327
+ });
328
+ };
329
+
330
+ const hideSecuritySettingsForNonAdmin = () => {
331
+ if (isAdminUser()) return;
332
+ Array.from(document.querySelectorAll('.MuiDialog-paper, [role="dialog"]')).forEach(dialog => {
333
+ const dialogText = textOfElement(dialog);
334
+ if (!/(eos security|nexowatt security|legacy admin|alter admin|protected adapters|geschutzte adapter|eos admin groups)/i.test(dialogText)) return;
335
+ dialog.classList.add('eos-security-settings-restricted');
336
+ const needles = /(eos security|nexowatt security|legacy admin|alter admin|protected adapters|geschutzte adapter|eos admin groups|lock legacy admin|hide legacy admin|restrict protected adapter)/i;
337
+ Array.from(dialog.querySelectorAll('label, legend, h2, h3, h4, .MuiTypography-root, .MuiFormLabel-root')).forEach(label => {
338
+ if (!needles.test(label.textContent || '')) return;
339
+ const row = label.closest('.MuiGrid2-root, .MuiGrid-root, .MuiFormControl-root, .MuiBox-root, .MuiPaper-root') || label.parentElement;
340
+ if (row && row !== dialog) row.classList.add('eos-security-admin-only-field');
341
+ });
342
+ if (!dialog.querySelector('.eos-security-restricted-note')) {
343
+ const note = document.createElement('div');
344
+ note.className = 'eos-security-restricted-note';
345
+ note.innerHTML = '<strong>EOS Systemschutz aktiv</strong><span>Diese Sicherheitseinstellungen sind nur für Administratoren sichtbar und änderbar.</span>';
346
+ const content = dialog.querySelector('.MuiDialogContent-root') || dialog;
347
+ content.insertBefore(note, content.firstElementChild || null);
348
+ }
349
+ });
350
+ };
351
+
352
+ const applySecurityUiGuard = () => safe(() => {
353
+ const policy = state.securityPolicy;
354
+ applySecurityClasses();
355
+ if (!policy.loaded) return;
356
+ if (isAdminUser()) {
357
+ document.querySelectorAll('.eos-hidden-legacy-admin, .eos-protected-adapter-row').forEach(el => {
358
+ el.classList.remove('eos-hidden-legacy-admin', 'eos-protected-adapter-row');
359
+ el.removeAttribute('aria-hidden');
360
+ });
361
+ return;
362
+ }
363
+
364
+ const containers = getSecurityContainers();
365
+ containers.forEach(container => {
366
+ if (policy.hideLegacyAdminForNonAdmins !== false && isLegacyAdminContainer(container)) {
367
+ container.classList.add('eos-hidden-legacy-admin');
368
+ container.setAttribute('aria-hidden', 'true');
369
+ return;
370
+ }
371
+ if (policy.restrictProtectedAdapterControls !== false) {
372
+ const protectedHit = (policy.protectedAdapters || []).some(adapter => adapter !== 'admin' && containerMentionsAdapter(container, adapter));
373
+ if (protectedHit) {
374
+ container.classList.add('eos-protected-adapter-row');
375
+ lockDeleteControls(container);
376
+ }
377
+ }
378
+ });
379
+ protectDeleteDialogs();
380
+ hideSecuritySettingsForNonAdmin();
381
+ });
166
382
 
167
383
  const isLoginView = () => safe(() => {
168
384
  const hasApp = !!document.getElementById('app-paper');
@@ -534,6 +750,7 @@
534
750
  ensureSettingsDialogClasses();
535
751
  hideNativeLogoutNav();
536
752
  hideOfficialNexoWattRepoWarning();
753
+ applySecurityUiGuard();
537
754
  patchTextNodes(document.body || document.documentElement);
538
755
  patchAttributes(document.body || document.documentElement);
539
756
  };
@@ -550,6 +767,7 @@
550
767
  ensureSettingsDialogClasses();
551
768
  hideNativeLogoutNav();
552
769
  hideOfficialNexoWattRepoWarning();
770
+ applySecurityUiGuard();
553
771
  for (const scope of scopes.slice(0, 80)) {
554
772
  if (!scope || !scope.isConnected) continue;
555
773
  patchTextNodes(scope);
@@ -605,11 +823,13 @@
605
823
  if (document.readyState === 'loading') {
606
824
  document.addEventListener('DOMContentLoaded', () => {
607
825
  fullPatch();
826
+ fetchSecurityPolicy();
608
827
  installObserver();
609
828
  [250, 1000, 2500, 5000].forEach(scheduleFullPatch);
610
829
  }, { once: true });
611
830
  } else {
612
831
  fullPatch();
832
+ fetchSecurityPolicy();
613
833
  installObserver();
614
834
  [250, 1000, 2500, 5000].forEach(scheduleFullPatch);
615
835
  }
@@ -0,0 +1,212 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const VERSION = 'v25-security-visibility-guard';
5
+ const LEGACY_ADMIN = 'admin';
6
+ const LEGACY_ADMIN_INSTANCE = 'admin.0';
7
+ const SECURITY_URL = '/nexowatt/security/session';
8
+
9
+ const state = {
10
+ loaded: false,
11
+ policy: {
12
+ isAdmin: false,
13
+ isEosAdminGroup: false,
14
+ isAdministrator: false,
15
+ hideLegacyAdminFromNonAdmins: true,
16
+ restrictProtectedAdapterControls: true,
17
+ protectedAdapters: ['eos-admin'],
18
+ },
19
+ scheduled: false,
20
+ observer: null,
21
+ };
22
+
23
+ const normalize = value => String(value || '').replace(/\s+/g, ' ').trim();
24
+ const normalizeAdapter = value => {
25
+ let adapter = String(value || '').trim().toLowerCase();
26
+ adapter = adapter.replace(/^system\.adapter\./, '').replace(/^iobroker\./, '').replace(/^@nexowatt\/iobroker\./, '').replace(/^@nexowatt\//, '');
27
+ adapter = adapter.replace(/\.\d+$/, '');
28
+ return /^[a-z0-9_-]+$/.test(adapter) ? adapter : '';
29
+ };
30
+
31
+ const isAdminUser = () => !!(state.policy?.isAdmin || state.policy?.isEosAdminGroup || state.policy?.isAdministrator);
32
+ const protectedAdapters = () => new Set((state.policy?.protectedAdapters || []).map(normalizeAdapter).filter(Boolean));
33
+
34
+ const isLegacyAdminId = value => {
35
+ const text = String(value || '').toLowerCase();
36
+ return text === LEGACY_ADMIN || text === LEGACY_ADMIN_INSTANCE || text === 'system.adapter.admin' || text === 'system.adapter.admin.0';
37
+ };
38
+
39
+ const isProtectedAdapter = value => {
40
+ const adapter = normalizeAdapter(value);
41
+ return !!adapter && protectedAdapters().has(adapter);
42
+ };
43
+
44
+ window.NEXOWATT_EOS_SECURITY = {
45
+ version: VERSION,
46
+ getPolicy: () => state.policy,
47
+ isAdminUser,
48
+ isProtectedAdapter,
49
+ shouldBlockAdapterDelete(adapterName) {
50
+ if (isAdminUser()) return false;
51
+ const adapter = normalizeAdapter(adapterName);
52
+ return adapter === LEGACY_ADMIN || isProtectedAdapter(adapter);
53
+ },
54
+ shouldBlockInstanceDelete(instanceIdOrAdapter) {
55
+ if (isAdminUser()) return false;
56
+ const raw = String(instanceIdOrAdapter || '').replace(/^system\.adapter\./, '');
57
+ const adapter = normalizeAdapter(raw);
58
+ return adapter === LEGACY_ADMIN || isProtectedAdapter(adapter);
59
+ },
60
+ };
61
+
62
+ const closestPanel = el => {
63
+ if (!el || !el.closest) return null;
64
+ return el.closest('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root, .MuiBox-root');
65
+ };
66
+
67
+ const markHidden = el => {
68
+ if (!el || el.classList?.contains('eos-security-keep-visible')) return;
69
+ el.classList.add('eos-security-hidden');
70
+ el.setAttribute('aria-hidden', 'true');
71
+ el.style.display = 'none';
72
+ };
73
+
74
+ const elementHasLegacyAdminIcon = el => {
75
+ if (!el?.querySelectorAll) return false;
76
+ return Array.from(el.querySelectorAll('img,[src]')).some(img => {
77
+ const src = String(img.getAttribute('src') || '').toLowerCase();
78
+ return /(?:^|\/)adapter\/admin\//.test(src) || /(?:^|\/)admin\/(?:admin\.(?:png|svg)|admin\.svg)/.test(src);
79
+ });
80
+ };
81
+
82
+ const elementTextMatchesLegacyAdmin = el => {
83
+ const text = normalize(el?.textContent || '').toLowerCase();
84
+ if (!text) return false;
85
+ if (text.includes('eos-admin') || text.includes('eos admin')) return false;
86
+ return /\badmin\.0\b/.test(text) || /system\.adapter\.admin(?:\.0)?\b/.test(text) || /\biobroker\.admin\b/.test(text);
87
+ };
88
+
89
+ const hideLegacyAdminPanels = () => {
90
+ if (!state.policy?.hideLegacyAdminFromNonAdmins || isAdminUser()) return;
91
+ const candidates = new Set();
92
+ document.querySelectorAll('img,[src]').forEach(img => {
93
+ if (elementHasLegacyAdminIcon(img.parentElement || img)) candidates.add(closestPanel(img) || img);
94
+ });
95
+ document.querySelectorAll('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root').forEach(el => {
96
+ if (elementHasLegacyAdminIcon(el) || elementTextMatchesLegacyAdmin(el)) candidates.add(el);
97
+ });
98
+ candidates.forEach(el => {
99
+ const text = normalize(el.textContent || '').toLowerCase();
100
+ if (text.includes('eos-admin') || text.includes('eos admin')) return;
101
+ markHidden(el);
102
+ });
103
+ };
104
+
105
+ const hideProtectedDeleteControls = () => {
106
+ if (!state.policy?.restrictProtectedAdapterControls || isAdminUser()) return;
107
+ const protectedSet = protectedAdapters();
108
+ if (!protectedSet.size) return;
109
+
110
+ document.querySelectorAll('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root').forEach(panel => {
111
+ let adapter = '';
112
+ const icon = panel.querySelector('img[src*="/adapter/"], img[src*="adapter/"]');
113
+ const src = icon ? String(icon.getAttribute('src') || '') : '';
114
+ const match = src.match(/adapter\/([^\/]+)\//i);
115
+ if (match) adapter = normalizeAdapter(match[1]);
116
+ const text = normalize(panel.textContent || '').toLowerCase();
117
+ if (!adapter) {
118
+ for (const protectedName of protectedSet) {
119
+ if (new RegExp(`\\b${protectedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\.\\d+)?\\b`, 'i').test(text)) {
120
+ adapter = protectedName;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ if (!adapter || !protectedSet.has(adapter)) return;
126
+ panel.classList.add('eos-security-protected-adapter');
127
+ panel.querySelectorAll('button,[role="button"],a').forEach(button => {
128
+ const label = normalize(`${button.textContent || ''} ${button.getAttribute('title') || ''} ${button.getAttribute('aria-label') || ''}`).toLowerCase();
129
+ if (/löschen|delete|remove|deinstall|uninstall/.test(label)) {
130
+ button.classList.add('eos-security-hidden-delete');
131
+ button.setAttribute('disabled', 'disabled');
132
+ button.setAttribute('aria-disabled', 'true');
133
+ button.style.display = 'none';
134
+ }
135
+ });
136
+ });
137
+
138
+ document.querySelectorAll('[role="menuitem"], .MuiMenuItem-root').forEach(item => {
139
+ const text = normalize(item.textContent || '').toLowerCase();
140
+ if (/löschen|delete|remove|deinstall|uninstall/.test(text)) {
141
+ // Context menus cannot always be mapped back to the originating card. For protected NexoWatt systems,
142
+ // deletion through pop-up menus is blocked by the command guard and visually hidden for non-admin users.
143
+ item.classList.add('eos-security-hidden-delete');
144
+ item.style.display = 'none';
145
+ }
146
+ });
147
+ };
148
+
149
+ const applyPolicyToDom = () => {
150
+ document.documentElement.classList.toggle('eos-security-admin-user', isAdminUser());
151
+ document.documentElement.classList.toggle('eos-security-non-admin-user', !isAdminUser());
152
+ hideLegacyAdminPanels();
153
+ hideProtectedDeleteControls();
154
+ };
155
+
156
+ const scheduleApply = () => {
157
+ if (state.scheduled) return;
158
+ state.scheduled = true;
159
+ const run = () => {
160
+ state.scheduled = false;
161
+ applyPolicyToDom();
162
+ };
163
+ if ('requestIdleCallback' in window) window.requestIdleCallback(run, { timeout: 600 });
164
+ else window.requestAnimationFrame(run);
165
+ };
166
+
167
+ const loadPolicy = async () => {
168
+ try {
169
+ const response = await fetch(SECURITY_URL, { credentials: 'same-origin', cache: 'no-store' });
170
+ const policy = await response.json();
171
+ state.policy = { ...state.policy, ...policy };
172
+ state.loaded = true;
173
+ } catch (e) {
174
+ state.loaded = false;
175
+ state.policy = { ...state.policy, isAdmin: false, isEosAdminGroup: false, isAdministrator: false };
176
+ console.warn('[NexoWatt EOS] Cannot read security policy, using safe non-admin UI mode:', e);
177
+ }
178
+ scheduleApply();
179
+ };
180
+
181
+ const installObserver = () => {
182
+ if (state.observer) return;
183
+ state.observer = new MutationObserver(() => scheduleApply());
184
+ state.observer.observe(document.documentElement, { childList: true, subtree: true });
185
+ };
186
+
187
+ document.addEventListener('click', event => {
188
+ const target = event.target?.closest?.('button,[role="button"],a,[role="menuitem"],.MuiMenuItem-root');
189
+ if (!target || isAdminUser()) return;
190
+ const label = normalize(`${target.textContent || ''} ${target.getAttribute?.('title') || ''} ${target.getAttribute?.('aria-label') || ''}`).toLowerCase();
191
+ if (/löschen|delete|remove|deinstall|uninstall/.test(label)) {
192
+ target.classList.add('eos-security-hidden-delete');
193
+ target.style.display = 'none';
194
+ }
195
+ }, true);
196
+
197
+ if (document.readyState === 'loading') {
198
+ document.addEventListener('DOMContentLoaded', () => {
199
+ loadPolicy();
200
+ installObserver();
201
+ [300, 1000, 2500, 5000].forEach(ms => window.setTimeout(scheduleApply, ms));
202
+ }, { once: true });
203
+ } else {
204
+ loadPolicy();
205
+ installObserver();
206
+ [300, 1000, 2500, 5000].forEach(ms => window.setTimeout(scheduleApply, ms));
207
+ }
208
+ window.addEventListener('hashchange', () => {
209
+ loadPolicy();
210
+ scheduleApply();
211
+ });
212
+ })();
@@ -1 +1 @@
1
- {"version":3,"file":"testPassword.js","sourceRoot":"./src/","sources":["lib/testPassword.ts"],"names":[],"mappings":";;AAAA,iDAAgD;AAChD,qDAA6E;AAC7E,6CAAuC;AAEvC,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,kDAAkD;AAClD,wEAAwE;AACxE,MAAM,aAAa,GAAG,IAAI,sBAAQ,CAAC;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;QACnC,IAAI,UAAU,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC5C,CAAC;QACD,QAAQ,EAAE,CAAC;IACf,CAAC;CACJ,CAAC,CAAC;AAEH,mEAAmE;AACnE,IAAA,wCAAuB,GAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;IACnC,IAAI,KAAK,EAAE,CAAC;QACR,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnE,qBAAqB;QACrB,MAAM,EAAE,GAAG,IAAA,+BAAe,EAAC;YACvB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,EAAE,CAAC,QAAQ,CAAC,2BAA2B,KAAK,CAAC,KAAK,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE;YACvF,UAAU,GAAG,KAAK,CAAC;YACnB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;gBACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;YACD,EAAE,CAAC,QAAQ,CAAC,4BAA4B,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,cAAsB,EAAE,EAAE;gBAChF,UAAU,GAAG,KAAK,CAAC;gBACnB,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACtE,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;oBAC9B,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;oBACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpB,CAAC;gBACD,EAAE,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,IAAA,iCAAgB,EAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;oBACvE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACrD,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;YACH,UAAU,GAAG,IAAI,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,UAAU,GAAG,IAAI,CAAC;IACtB,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"testPassword.js","sourceRoot":"./src/","sources":["lib/testPassword.ts"],"names":[],"mappings":";;AAAA,iDAAgD;AAChD,qDAA6E;AAC7E,6CAAuC;AAEvC,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,kDAAkD;AAClD,4EAA4E;AAC5E,MAAM,aAAa,GAAG,IAAI,sBAAQ,CAAC;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;QACnC,IAAI,UAAU,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC5C,CAAC;QACD,QAAQ,EAAE,CAAC;IACf,CAAC;CACJ,CAAC,CAAC;AAEH,mEAAmE;AACnE,IAAA,wCAAuB,GAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;IACnC,IAAI,KAAK,EAAE,CAAC;QACR,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnE,qBAAqB;QACrB,MAAM,EAAE,GAAG,IAAA,+BAAe,EAAC;YACvB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,EAAE,CAAC,QAAQ,CAAC,2BAA2B,KAAK,CAAC,KAAK,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE;YACvF,UAAU,GAAG,KAAK,CAAC;YACnB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;gBACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;YACD,EAAE,CAAC,QAAQ,CAAC,4BAA4B,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,cAAsB,EAAE,EAAE;gBAChF,UAAU,GAAG,KAAK,CAAC;gBACnB,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACtE,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;oBAC9B,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;oBACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpB,CAAC;gBACD,EAAE,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,IAAA,iCAAgB,EAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;oBACvE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACrD,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;YACH,UAAU,GAAG,IAAI,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,UAAU,GAAG,IAAI,CAAC;IACtB,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC,CAAC,CAAC"}