iobroker.eos-admin 7.10.4 → 7.10.6

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.
Files changed (44) hide show
  1. package/INSTALL_TEST_V7.10.6.md +8 -0
  2. package/MERGE_UPDATE_README_V7.10.6.md +7 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +19 -18
  4. package/NEXOWATT_EOS_PREBUILT_MANIFEST.json +40 -41
  5. package/PUBLISH_STABLE_V7.10.6.md +8 -0
  6. package/README_STABILITY_V7.10.6.md +5 -0
  7. package/RELEASE_ACCEPTANCE_V7.10.6.md +11 -0
  8. package/RELEASE_NOTES_V7.10.6.md +14 -0
  9. package/SECURITY_ROLE_MATRIX_V7.10.6.md +12 -0
  10. package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
  11. package/adminWww/assets/Objects-DPan0bzw-v84.js +1 -1
  12. package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
  13. package/adminWww/css/nexowatt-native-shell.css +12 -1
  14. package/adminWww/index.html +14 -15
  15. package/adminWww/js/eos-role-bootstrap.js +5 -5
  16. package/adminWww/js/eos-role-ui.js +211 -17
  17. package/adminWww/js/nexowatt-native-shell.js +1 -1
  18. package/adminWww/static/js/{nexowatt-stable-v7101.js → nexowatt-stable-v7106.js} +1 -1
  19. package/build/lib/web.js +34 -0
  20. package/build/main.js +13 -15
  21. package/io-package.json +32 -5
  22. package/package.json +11 -10
  23. package/tools/nexowatt-account-password-flow-selftest.cjs +3 -3
  24. package/tools/nexowatt-clean-legacy-runtime.cjs +6 -0
  25. package/tools/nexowatt-internal-reserve-selftest.cjs +1 -1
  26. package/tools/nexowatt-login-layout-selftest.cjs +3 -3
  27. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  28. package/tools/nexowatt-modern-ui-selftest.cjs +2 -2
  29. package/tools/nexowatt-role-security-selftest.cjs +147 -41
  30. package/tools/nexowatt-runtime-cleanup-selftest.cjs +4 -0
  31. package/tools/nexowatt-stable-v7103-selftest.cjs +4 -4
  32. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +2 -2
  33. package/tools/nexowatt-user-area-selftest.cjs +36 -0
  34. package/tools/nexowatt-validate-package.cjs +9 -8
  35. package/INSTALL_TEST_V7.10.4.md +0 -9
  36. package/MERGE_UPDATE_README_V7.10.4.md +0 -3
  37. package/PUBLISH_STABLE_V7.10.4.md +0 -10
  38. package/README_STABILITY_V7.10.4.md +0 -12
  39. package/RELEASE_ACCEPTANCE_V7.10.4.md +0 -3
  40. package/RELEASE_NOTES_V7.10.4.md +0 -11
  41. package/adminWww/nexowatt-role-security.js +0 -336
  42. package/build/lib/eosRoleSecurity.js +0 -310
  43. package/src-admin/README.md +0 -69
  44. package/src-admin/nexowatt-role-security.js +0 -336
@@ -1184,12 +1184,23 @@ html.eos-login .eos-login-card-modern {
1184
1184
  /* Internal fallback services are never exposed outside Admin / Service. */
1185
1185
  html.eos-role-installer [data-instance^="admin."],
1186
1186
  html.eos-role-installer [data-instance^="backitup."],
1187
+ html.eos-role-installer [data-instance^="xterm."],
1187
1188
  html.eos-role-enduser [data-instance^="admin."],
1188
1189
  html.eos-role-enduser [data-instance^="backitup."],
1190
+ html.eos-role-enduser [data-instance^="xterm."],
1189
1191
  html.eos-role-installer [data-instance-id^="admin."],
1190
1192
  html.eos-role-installer [data-instance-id^="backitup."],
1193
+ html.eos-role-installer [data-instance-id^="xterm."],
1191
1194
  html.eos-role-enduser [data-instance-id^="admin."],
1192
- html.eos-role-enduser [data-instance-id^="backitup."] { display: none !important; }
1195
+ html.eos-role-enduser [data-instance-id^="backitup."],
1196
+ html.eos-role-enduser [data-instance-id^="xterm."] { display: none !important; }
1197
+
1198
+ html.eos-role-enduser #app-paper[data-eos-datapoints-readonly] [data-eos-object-writable="1"],
1199
+ html.eos-role-enduser #app-paper[data-eos-datapoints-readonly] [data-eos-enduser-write-blocked="1"] {
1200
+ pointer-events: none !important;
1201
+ cursor: default !important;
1202
+ opacity: 0.62 !important;
1203
+ }
1193
1204
 
1194
1205
 
1195
1206
 
@@ -8,7 +8,7 @@
8
8
  <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
9
9
  <meta http-equiv="Pragma" content="no-cache" />
10
10
  <meta http-equiv="Expires" content="0" />
11
- <script src="./js/eos-manual-write-policy.js?v=7101"></script>
11
+ <script src="./js/eos-manual-write-policy.js?v=7106"></script>
12
12
  <script type="module" src="./assets/hostInit-v84.js?v=84"></script>
13
13
 
14
14
  <meta charset="utf-8" />
@@ -36,8 +36,8 @@
36
36
  rel="stylesheet"
37
37
  href="css/leaflet.css"
38
38
  />
39
- <link rel="stylesheet" href="./css/nexowatt-native-shell.css?v=7101" />
40
- <link rel="stylesheet" href="./css/eos-ems-overview.css?v=7101" />
39
+ <link rel="stylesheet" href="./css/nexowatt-native-shell.css?v=7106" />
40
+ <link rel="stylesheet" href="./css/eos-ems-overview.css?v=7106" />
41
41
  <link rel="stylesheet" href="./css/eos-auto-update.css?v=7103" />
42
42
  <link
43
43
  rel="manifest"
@@ -160,23 +160,22 @@
160
160
  </script>
161
161
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js">
162
162
  <link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js">
163
- <script defer src="./js/eos-role-bootstrap.js?v=7101" data-eos-entry="./assets/index-CQZugZ1z-v84.js?v=84"></script>
164
- <script defer src="./js/eos-policy-client.js?v=7101"></script>
165
- <script defer src="./js/eos-branding-sanitizer.js?v=7101"></script>
166
- <script defer src="./js/nexowatt-native-shell.js?v=7101"></script>
167
- <script defer src="./js/eos-native-security.js?v=7101"></script>
168
- <script defer src="./js/eos-basic-settings.js?v=7101"></script>
169
- <script defer src="./js/eos-role-ui.js?v=7101"></script>
170
- <script defer src="./js/eos-ems-overview.js?v=7101"></script>
163
+ <script defer src="./js/eos-role-bootstrap.js?v=7106" data-eos-entry="./assets/index-CQZugZ1z-v84.js?v=84"></script>
164
+ <script defer src="./js/eos-policy-client.js?v=7106"></script>
165
+ <script defer src="./js/eos-branding-sanitizer.js?v=7106"></script>
166
+ <script defer src="./js/nexowatt-native-shell.js?v=7106"></script>
167
+ <script defer src="./js/eos-native-security.js?v=7106"></script>
168
+ <script defer src="./js/eos-basic-settings.js?v=7106"></script>
169
+ <script defer src="./js/eos-role-ui.js?v=7106"></script>
170
+ <script defer src="./js/eos-ems-overview.js?v=7106"></script>
171
171
 
172
172
  <script defer src="./js/eos-auto-update.js?v=7103"></script>
173
- <script defer src="./js/eos-assistant.js?v=7101"></script>
173
+ <script defer src="./js/eos-assistant.js?v=7106"></script>
174
174
  </head>
175
175
  <body>
176
176
  <noscript>Für NexoWatt EOS muss JavaScript aktiviert sein.</noscript>
177
177
  <div id="root"></div>
178
- <script defer src="static/js/nexowatt-stable-login.js?v=7101"></script>
179
- <script defer src="static/js/nexowatt-stable-v7101.js?v=7101"></script>
180
- <script defer src="./nexowatt-role-security.js?v=7104"></script>
178
+ <script defer src="static/js/nexowatt-stable-login.js?v=7106"></script>
179
+ <script defer src="static/js/nexowatt-stable-v7106.js?v=7106"></script>
181
180
  </body>
182
181
  </html>
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v7101-standard-password-native-overview';
4
+ const VERSION = 'v7106-admin-authoritative-rbac';
5
5
  const script = document.currentScript || document.querySelector('script[src*="eos-role-bootstrap.js"]');
6
6
  const entry = script?.dataset?.eosEntry || '';
7
7
  let launched = false;
@@ -16,7 +16,7 @@
16
16
 
17
17
  const roleFromPolicy = policy => {
18
18
  const raw = normalize(policy?.role || policy?.eosRole || policy?.accessRole || '');
19
- if (policy?.isAdmin || policy?.isEosAdminGroup || policy?.isAdministrator || /^(?:admin|administrator)$/.test(raw)) return 'admin';
19
+ if (policy?.user === 'system.user.admin' || policy?.userId === 'system.user.admin' || policy?.isAdmin || policy?.isEosAdminGroup || policy?.isAdministrator || /^(?:admin|administrator)$/.test(raw)) return 'admin';
20
20
  if (/nexowatt service|eos service|service admin|service administrator/.test(raw)) return 'admin';
21
21
  if (policy?.isInstaller || /installateur|installer|installation|inbetriebnahme|techniker|technician|integrator|partner/.test(raw)) return 'installer';
22
22
  return 'enduser';
@@ -25,11 +25,11 @@
25
25
  const isCustomerBackupTab = tab => /^(?:tab-)?(?:nexowatt-backup|eos-backup|nexowatt-sicherung)(?:-|$)/.test(normalize(tab));
26
26
  const isReleasedEndUserTab = tab => /^(?:tab-)?(?:nexowatt-ui|nexowatt-cockpit|eos-cockpit|eos-dashboard|kunden-cockpit|endkunden-cockpit|lovelace|jarvis|vis|iqontrol|material)(?:-|$)/.test(normalize(tab));
27
27
  const isOfficialReserveTab = tab => /^(?:tab-)?(?:admin|backitup)(?:-\d+)?$/.test(normalize(tab));
28
- const isInstallerDenied = tab => /(?:tab-hosts|tab-files|tab-xterm|tab-xtrem|tab-system|hosts|files|console|terminal|security)/.test(normalize(tab)) || isOfficialReserveTab(tab);
28
+ const isInstallerDenied = tab => /(?:tab-users|tab-hosts|tab-files|tab-xterm|tab-xtrem|tab-system|users|hosts|files|console|terminal|security)/.test(normalize(tab)) || isOfficialReserveTab(tab);
29
29
  const isAllowed = (role, route) => route !== 'easy' && (role === 'admin'
30
30
  || (role === 'installer'
31
- ? route === 'tab-intro' || route === 'tab-users' || isCustomerBackupTab(route) || !isInstallerDenied(route)
32
- : route === 'tab-intro' || route === 'tab-enums' || isCustomerBackupTab(route) || isReleasedEndUserTab(route)));
31
+ ? route === 'tab-intro' || isCustomerBackupTab(route) || !isInstallerDenied(route)
32
+ : route === 'tab-intro' || route === 'tab-enums' || route === 'tab-objects' || isCustomerBackupTab(route) || isReleasedEndUserTab(route)));
33
33
  const defaultTab = () => 'tab-intro';
34
34
  const currentRoute = () => {
35
35
  const hash = decodeURIComponent(window.location.hash || '').toLowerCase();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v7101-native-overview-standard-users';
4
+ const VERSION = 'v7106-admin-authoritative-rbac';
5
5
  window.NEXOWATT_EOS_ROLE_UI_VERSION = VERSION;
6
6
 
7
7
  const state = {
@@ -15,10 +15,13 @@
15
15
  reserveObserver: null,
16
16
  reservePaper: null,
17
17
  reserveScheduled: false,
18
+ readonlySystemInfo: null,
19
+ readonlySystemInfoPromise: null,
18
20
  };
19
21
  const abort = new AbortController();
20
22
 
21
23
  const safe = fn => { try { return fn(); } catch (_) { return undefined; } };
24
+ const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, match => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[match] || match));
22
25
  const normalize = value => String(value || '')
23
26
  .toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/ß/g, 'ss')
24
27
  .replace(/[^a-z0-9#/_:.-]+/g, ' ').trim();
@@ -37,7 +40,7 @@
37
40
 
38
41
  const roleFromPolicy = policy => {
39
42
  const raw = normalize(policy?.role || policy?.eosRole || policy?.accessRole || '');
40
- if (policy?.isAdmin || policy?.isEosAdminGroup || policy?.isAdministrator || /^(?:admin|administrator)$/.test(raw)) return 'admin';
43
+ if (policy?.user === 'system.user.admin' || policy?.userId === 'system.user.admin' || policy?.isAdmin || policy?.isEosAdminGroup || policy?.isAdministrator || /^(?:admin|administrator)$/.test(raw)) return 'admin';
41
44
  if (/nexowatt service|eos service|service admin|service administrator/.test(raw)) return 'admin';
42
45
  if (policy?.isInstaller || /installateur|installer|installation|inbetriebnahme|techniker|technician|integrator|partner/.test(raw)) return 'installer';
43
46
  return 'enduser';
@@ -47,10 +50,10 @@
47
50
  const isOfficialReserveTab = tab => isOfficialAdminTab(tab) || isOfficialBackupTab(tab);
48
51
  const isCustomerBackupTab = tab => /^tab-(?:nexowatt-backup|eos-backup|nexowatt-sicherung)(?:-\d+)?$/.test(normalize(tab));
49
52
  const endUserAllowedByText = value => /(eos cockpit|nexowatt ui|nexowatt cockpit|kunden cockpit|kundenbereich|endkundenbereich|visualisierung|visualisation|visu|dashboard|energie cockpit|energy cockpit|bedienung|smart home|smart-home|lovelace|jarvis|iqontrol|material)/.test(normalize(value));
50
- const endUserDeniedByText = value => /(module|dienste|datenpunkte|objekte|systemlogs|logs|zugange|rechte|benutzer|users|gruppen|groups|system hosts|system-hosts|hosts|dateien|files|konsole|console|terminal|xterm|adapter|instances?|instanzen|geraetesuche|discovery|systemschutz|security)/.test(normalize(value));
53
+ const endUserDeniedByText = value => /(module|dienste|systemlogs|logs|zugange|rechte|benutzer|users|gruppen|groups|system hosts|system-hosts|hosts|dateien|files|konsole|console|terminal|xterm|adapter|instances?|instanzen|geraetesuche|discovery|systemschutz|security)/.test(normalize(value));
51
54
  const isEndUserTab = (tab, label = '') => {
52
55
  const clean = normalize(tab); const text = `${clean} ${normalize(label)}`;
53
- if (clean === 'tab-intro' || clean === 'tab-enums' || isCustomerBackupTab(clean)) return true;
56
+ if (clean === 'tab-intro' || clean === 'tab-enums' || clean === 'tab-objects' || isCustomerBackupTab(clean)) return true;
54
57
  if (/^tab-(nexowatt-ui|nexowatt-cockpit|eos-cockpit|eos-dashboard|kunden-cockpit|endkunden-cockpit|lovelace|jarvis|vis|iqontrol|material)(?:-|$)/.test(clean)) return true;
55
58
  return !!clean && !endUserDeniedByText(text) && endUserAllowedByText(text);
56
59
  };
@@ -58,14 +61,14 @@
58
61
  const clean = normalize(tab); const text = `${clean} ${normalize(label)}`;
59
62
  if (isOfficialReserveTab(clean)) return true;
60
63
  if (isCustomerBackupTab(clean)) return false;
61
- return /(?:tab-hosts|tab-files|tab-xterm|tab-xtrem|tab-system|system-hosts|system hosts|hosts|dateien|files|konsole|console|terminal|xterm|systemschutz|security)/.test(text);
64
+ return /(?:tab-users|tab-hosts|tab-files|tab-xterm|tab-xtrem|tab-system|users|benutzer|gruppen|groups|system-hosts|system hosts|hosts|dateien|files|konsole|console|terminal|xterm|systemschutz|security)/.test(text);
62
65
  };
63
66
  const isRouteAllowed = (role, route, label = '') => {
64
67
  if (route === 'easy') return false;
65
68
  if (role === 'admin') return true;
66
69
  if (isOfficialReserveTab(route)) return false;
67
- if (role === 'installer') return route === 'tab-intro' || route === 'tab-users' || !isInstallerDenied(route, label);
68
- if (role === 'enduser') return route === 'tab-users' || isEndUserTab(route, label);
70
+ if (role === 'installer') return route === 'tab-intro' || (route !== 'tab-users' && !isInstallerDenied(route, label));
71
+ if (role === 'enduser') return isEndUserTab(route, label);
69
72
  return false;
70
73
  };
71
74
  const defaultTab = () => 'tab-intro';
@@ -133,7 +136,7 @@
133
136
  return normalize(`${button?.getAttribute?.('aria-label') || ''} ${button?.getAttribute?.('title') || ''} ${testIds}`);
134
137
  };
135
138
  const isSystemSettingsButton = button => /buildicon|system settings|systemeinstellungen|systeeminstellingen/.test(toolbarButtonSignature(button));
136
- const isExpertModeButton = button => /expert|experten/.test(toolbarButtonSignature(button));
139
+ const isExpertModeButton = button => button?.dataset?.eosExpertControl === '1' || /expert|experten/.test(toolbarButtonSignature(button));
137
140
  const markAdminOnlyToolbarControls = () => safe(() => {
138
141
  const toolbar = document.querySelector('.eos-top-toolbar,.MuiAppBar-root .MuiToolbar-root');
139
142
  if (!toolbar) return;
@@ -226,7 +229,6 @@
226
229
  ['Datenpunkte', 'Messwerte prüfen und freigegebene Werte schreiben', 'tab-objects', 'datapoints'],
227
230
  ['Struktur', 'Räume und Funktionen für Smart Home zuordnen', 'tab-enums', 'structure'],
228
231
  ['Geräte', 'Geräteintegration und Inbetriebnahme öffnen', 'tab-devicemanager', 'devices'],
229
- ['Zugänge & Rechte', 'Passwörter im nativen Benutzereditor verwalten', 'tab-users', 'rights'],
230
232
  ['Basis-Einstellungen', 'Standort, Sprache und Anlagenparameter', 'basic-settings', 'settings'],
231
233
  ...(backupTab ? [['NexoWatt Sicherung', 'Sicherung und Wiederherstellung des EOS-Systems', backupTab, 'backup']] : []),
232
234
  ['EOS Cockpit', 'Energie-, Lade- und Gebäudesteuerung öffnen', uiTab, 'eos'],
@@ -234,6 +236,7 @@
234
236
  }
235
237
  return [
236
238
  ['EOS Cockpit', 'Energie, Laden und Gebäude bedienen', uiTab, 'eos'],
239
+ ['Datenpunkte', 'Messwerte und Zustände ausschließlich lesen', 'tab-objects', 'datapoints'],
237
240
  ['Smart Home', 'Räume, Funktionen und freigegebene Geräte zuordnen', 'tab-enums', 'structure'],
238
241
  ...(backupTab ? [['NexoWatt Sicherung', 'Eigene Sicherungen ausführen und wiederherstellen', backupTab, 'backup']] : []),
239
242
  ];
@@ -268,6 +271,55 @@
268
271
  role.setAttribute('data-nexowatt-overview-role', state.role);
269
272
  }
270
273
  };
274
+
275
+ const fetchReadonlySystemInfo = () => {
276
+ if (state.readonlySystemInfo) return Promise.resolve(state.readonlySystemInfo);
277
+ if (state.readonlySystemInfoPromise) return state.readonlySystemInfoPromise;
278
+ state.readonlySystemInfoPromise = fetch('/nexowatt/readonly/system-info', {
279
+ credentials: 'same-origin',
280
+ cache: 'no-store',
281
+ headers: { Accept: 'application/json' },
282
+ })
283
+ .then(response => (response.ok ? response.json() : null))
284
+ .then(data => {
285
+ state.readonlySystemInfo = data && typeof data === 'object' ? data : null;
286
+ return state.readonlySystemInfo;
287
+ })
288
+ .catch(() => null)
289
+ .finally(() => {
290
+ state.readonlySystemInfoPromise = null;
291
+ scheduleApply();
292
+ });
293
+ return state.readonlySystemInfoPromise;
294
+ };
295
+ const applyReadonlySystemInfoCard = () => safe(() => {
296
+ if (state.role !== 'enduser' || currentRoute() !== 'tab-intro') return;
297
+ const paper = appPaper();
298
+ if (!paper) return;
299
+ const title = Array.from(paper.querySelectorAll('h1,h2,h3,h4,h5,h6')).find(node => normalize(node.textContent || '') === 'nexowatt');
300
+ const card = title?.closest?.('.MuiCard-root');
301
+ if (!card || !/platform|ram|node\.js|npm/i.test(String(card.textContent || ''))) return;
302
+ Array.from(card.querySelectorAll('button')).forEach(button => {
303
+ if (/info/i.test(textOf(button))) {
304
+ button.classList.add('eos-role-hidden');
305
+ button.setAttribute('aria-hidden', 'true');
306
+ button.disabled = true;
307
+ }
308
+ });
309
+ if (!state.readonlySystemInfo && !state.readonlySystemInfoPromise) {
310
+ void fetchReadonlySystemInfo();
311
+ return;
312
+ }
313
+ const data = state.readonlySystemInfo;
314
+ const list = card.querySelector('ul');
315
+ if (!data || !list) return;
316
+ list.innerHTML = `
317
+ <li><span><span style="font-weight:600">Platform: </span>${escapeHtml(data.platform || '--')}</span></li>
318
+ <li><span><span style="font-weight:600">RAM: </span>${escapeHtml(Number.isFinite(Number(data.ramMb)) ? `${Math.round(Number(data.ramMb))} MB` : '--')}</span></li>
319
+ <li><span><span style="font-weight:600">Node.js: </span>${escapeHtml(data.nodejs || '--')}</span></li>
320
+ <li><span><span style="font-weight:600">NPM: </span>${escapeHtml(data.npm || '--')}</span></li>`;
321
+ });
322
+
271
323
  const hideIntroEditControls = () => {
272
324
  const paper = appPaper();
273
325
  if (!paper) return;
@@ -320,7 +372,7 @@
320
372
  'tr', '.MuiTableRow-root', '.MuiAccordion-root', '.MuiCard-root', '.MuiListItem-root',
321
373
  '[role="row"]', '[data-instance-row]', '[data-testid="adapter-card"]', '[data-testid="instance-card"]',
322
374
  ].join(',');
323
- const reserveId = /^(?:system\.adapter\.)?(?:admin|backitup)\.\d+$/i;
375
+ const reserveId = /^(?:system\.adapter\.)?(?:admin|backitup|xterm)\.\d+$/i;
324
376
  const normalizeInstance = value => String(value || '').trim().replace(/^system\.adapter\./i, '');
325
377
  const identityValues = node => {
326
378
  if (!node) return [];
@@ -337,7 +389,7 @@
337
389
  .replace(/nexowatt-backup\.\d+/gi, '')
338
390
  .replace(/eos-backup\.\d+/gi, '')
339
391
  .replace(/system\.adapter\.(?:eos-admin|nexowatt-backup|eos-backup)\.\d+/gi, '');
340
- return /(?:^|[\s|,;])(?:system\.adapter\.)?(?:admin|backitup)\.\d+(?=$|[\s|,;])/i.test(text.trim());
392
+ return /(?:^|[\s|,;])(?:system\.adapter\.)?(?:admin|backitup|xterm)\.\d+(?=$|[\s|,;])/i.test(text.trim());
341
393
  };
342
394
  const rowIsReserve = row => identityValues(row).some(value => reserveId.test(value)) || rowTextContainsReserve(row);
343
395
  const setHidden = (row, hidden) => {
@@ -363,12 +415,12 @@
363
415
 
364
416
  paper.querySelectorAll(rowSelector).forEach(row => setHidden(row, rowIsReserve(row)));
365
417
  paper.querySelectorAll([
366
- '[data-id^="system.adapter.admin"]','[data-id^="system.adapter.backitup"]',
367
- '[data-instance^="admin."]','[data-instance^="backitup."]',
368
- '[data-instance-id^="admin."]','[data-instance-id^="backitup."]',
369
- '[data-name="admin"]','[data-name="backitup"]',
370
- 'a[href*="#tab-admin"]','a[href*="#tab-backitup"]',
371
- 'a[href*="system.adapter.admin"]','a[href*="system.adapter.backitup"]',
418
+ '[data-id^="system.adapter.admin"]','[data-id^="system.adapter.backitup"]','[data-id^="system.adapter.xterm"]',
419
+ '[data-instance^="admin."]','[data-instance^="backitup."]','[data-instance^="xterm."]',
420
+ '[data-instance-id^="admin."]','[data-instance-id^="backitup."]','[data-instance-id^="xterm."]',
421
+ '[data-name="admin"]','[data-name="backitup"]','[data-name="xterm"]',
422
+ 'a[href*="#tab-admin"]','a[href*="#tab-backitup"]','a[href*="#tab-xterm"]',
423
+ 'a[href*="system.adapter.admin"]','a[href*="system.adapter.backitup"]','a[href*="system.adapter.xterm"]',
372
424
  ].join(',')).forEach(node => setHidden(rowFor(node) || node, true));
373
425
 
374
426
  // Admin 7 table cells may have no metadata. Match only an exact visible instance ID and
@@ -429,6 +481,139 @@
429
481
  }
430
482
  });
431
483
 
484
+
485
+ const privilegedFrameObservers = new WeakMap();
486
+ const privilegedActionPattern = /^(?:ems apps?(?: offnen| öffnen)?|app center|app-center|appcenter|simulation|simulator|lizenz|license)$/;
487
+ const isPrivilegedAction = element => {
488
+ const signature = normalize(textOf(element));
489
+ const href = normalize(element?.getAttribute?.('href') || element?.href || element?.getAttribute?.('data-href') || '');
490
+ return privilegedActionPattern.test(signature)
491
+ || /(?:^|[\/#?&_.-])(?:appcenter|app-center|simulation|simulator|license|lizenz)(?:$|[\/#?&=_.-])/.test(href);
492
+ };
493
+ const setPrivilegedActionHidden = (element, hidden) => {
494
+ if (!element) return;
495
+ const target = element.closest?.('a,button,[role="button"],[role="tab"],.MuiButtonBase-root') || element;
496
+ if (hidden) {
497
+ target.dataset.eosPrivilegedActionHidden = '1';
498
+ target.classList.add('eos-role-hidden');
499
+ target.setAttribute('aria-hidden', 'true');
500
+ if ('disabled' in target) target.disabled = true;
501
+ } else if (target.dataset.eosPrivilegedActionHidden === '1') {
502
+ delete target.dataset.eosPrivilegedActionHidden;
503
+ target.classList.remove('eos-role-hidden');
504
+ target.removeAttribute('aria-hidden');
505
+ if ('disabled' in target) target.disabled = false;
506
+ }
507
+ };
508
+ const applyPrivilegedActionsInDocument = doc => safe(() => {
509
+ if (!doc?.documentElement) return;
510
+ const admin = state.role === 'admin';
511
+ doc.querySelectorAll('[data-eos-privileged-action-hidden="1"]').forEach(element => {
512
+ if (admin || !isPrivilegedAction(element)) setPrivilegedActionHidden(element, false);
513
+ });
514
+ for (const element of doc.querySelectorAll('a,button,[role="button"],[role="tab"],.MuiButtonBase-root')) {
515
+ if (isPrivilegedAction(element)) setPrivilegedActionHidden(element, !admin);
516
+ }
517
+ if (!doc.documentElement.dataset.eosPrivilegedClickGuard) {
518
+ doc.documentElement.dataset.eosPrivilegedClickGuard = '1';
519
+ doc.addEventListener('click', event => {
520
+ if (state.role === 'admin' || state.role === 'unknown') return;
521
+ const target = event.target?.closest?.('a,button,[role="button"],[role="tab"],.MuiButtonBase-root');
522
+ if (!target || !isPrivilegedAction(target)) return;
523
+ event.preventDefault();
524
+ event.stopImmediatePropagation();
525
+ }, true);
526
+ }
527
+ if (!privilegedFrameObservers.has(doc)) {
528
+ const observer = new MutationObserver(() => {
529
+ window.setTimeout(() => applyPrivilegedActionsInDocument(doc), 0);
530
+ });
531
+ observer.observe(doc.documentElement, { childList: true, subtree: true });
532
+ privilegedFrameObservers.set(doc, observer);
533
+ }
534
+ });
535
+ const applyPrivilegedIframePolicy = () => safe(() => {
536
+ applyPrivilegedActionsInDocument(document);
537
+ document.querySelectorAll('iframe').forEach(frame => {
538
+ const applyFrame = () => safe(() => applyPrivilegedActionsInDocument(frame.contentDocument));
539
+ if (frame.dataset.eosRoleFrameGuard !== '1') {
540
+ frame.dataset.eosRoleFrameGuard = '1';
541
+ frame.addEventListener('load', applyFrame);
542
+ }
543
+ applyFrame();
544
+ });
545
+ });
546
+ const scrubInheritedUiAdminSession = () => safe(() => {
547
+ if (state.role === 'admin' || state.role === 'unknown') return;
548
+ const user = String(state.policy?.user || state.policy?.userId || '').replace(/^system\.user\./, '') || state.role;
549
+ const key = `eosUiAdminSessionScrubbed:v7106:${user}:${state.role}`;
550
+ if (sessionStorage.getItem(key) === '1') return;
551
+ sessionStorage.setItem(key, '1');
552
+ const frame = document.createElement('iframe');
553
+ frame.hidden = true;
554
+ frame.tabIndex = -1;
555
+ frame.setAttribute('aria-hidden', 'true');
556
+ frame.src = `${window.location.protocol}//${window.location.hostname}:8188/logout?source=eos-admin&role=${encodeURIComponent(state.role)}&t=${Date.now()}`;
557
+ frame.addEventListener('load', () => window.setTimeout(() => frame.remove(), 1200), { once: true });
558
+ document.body?.appendChild(frame);
559
+ window.setTimeout(() => frame.remove(), 7000);
560
+ });
561
+ const applyEndUserDatapointPolicy = () => safe(() => {
562
+ const paper = appPaper();
563
+ if (!paper) return;
564
+ const readonly = state.role === 'enduser' && currentRoute() === 'tab-objects';
565
+ paper.toggleAttribute('data-eos-datapoints-readonly', readonly);
566
+ const clearControl = control => {
567
+ if (!control) return;
568
+ control.removeAttribute('aria-disabled');
569
+ control.removeAttribute('data-eos-enduser-write-blocked');
570
+ control.removeAttribute('data-eos-enduser-action-hidden');
571
+ control.classList.remove('eos-role-hidden');
572
+ control.removeAttribute('aria-hidden');
573
+ if ('disabled' in control) control.disabled = false;
574
+ };
575
+ paper.querySelectorAll('[data-eos-enduser-write-blocked="1"],[data-eos-enduser-action-hidden="1"]').forEach(clearControl);
576
+ if (!readonly) return;
577
+ const blockControl = control => {
578
+ if (!control) return;
579
+ control.setAttribute('aria-disabled', 'true');
580
+ control.setAttribute('data-eos-enduser-write-blocked', '1');
581
+ };
582
+ const hideControl = control => {
583
+ if (!control) return;
584
+ control.setAttribute('aria-disabled', 'true');
585
+ control.setAttribute('data-eos-enduser-action-hidden', '1');
586
+ control.classList.add('eos-role-hidden');
587
+ control.setAttribute('aria-hidden', 'true');
588
+ if ('disabled' in control) control.disabled = true;
589
+ };
590
+ paper.querySelectorAll('[data-eos-object-writable="1"]').forEach(blockControl);
591
+ paper.querySelectorAll([
592
+ 'button[title*="edit" i]','button[title*="delete" i]','button[title*="remove" i]','button[title*="setting" i]','button[title*="einstell" i]',
593
+ 'button[aria-label*="edit" i]','button[aria-label*="delete" i]','button[aria-label*="remove" i]','button[aria-label*="setting" i]','button[aria-label*="einstell" i]',
594
+ '[role="button"][title*="edit" i]','[role="button"][title*="delete" i]','[role="button"][title*="setting" i]',
595
+ '[role="button"][aria-label*="edit" i]','[role="button"][aria-label*="delete" i]','[role="button"][aria-label*="setting" i]'
596
+ ].join(',')).forEach(hideControl);
597
+ paper.querySelectorAll('svg[data-testid*="Edit"],svg[data-testid*="Delete"],svg[data-testid*="Settings"],svg[data-testid*="Tune"],svg[data-testid*="Build"]').forEach(icon => hideControl(icon.closest('button,[role="button"]')));
598
+ });
599
+ const clearLegacyHeuristicRestrictionsForAdmin = () => safe(() => {
600
+ if (state.role !== 'admin') return;
601
+ document.querySelectorAll('[data-eos-role-hidden]').forEach(element => {
602
+ element.removeAttribute('data-eos-role-hidden');
603
+ element.removeAttribute('aria-hidden');
604
+ element.style.removeProperty('display');
605
+ });
606
+ document.querySelectorAll('[data-eos-readonly],[data-eos-enduser-write-blocked],[data-eos-enduser-action-hidden]').forEach(element => {
607
+ element.removeAttribute('data-eos-readonly');
608
+ element.removeAttribute('data-eos-enduser-write-blocked');
609
+ element.removeAttribute('data-eos-enduser-action-hidden');
610
+ element.removeAttribute('aria-disabled');
611
+ element.style.removeProperty('pointer-events');
612
+ element.style.removeProperty('opacity');
613
+ if ('disabled' in element) element.disabled = false;
614
+ });
615
+ });
616
+
432
617
  const apply = () => {
433
618
  if (!state.policy || state.role === 'unknown') return;
434
619
  setRoleClasses(state.role);
@@ -436,10 +621,15 @@
436
621
  applyMenuPolicy();
437
622
  markAdminOnlyToolbarControls();
438
623
  applySensitiveDialogPolicy();
624
+ clearLegacyHeuristicRestrictionsForAdmin();
439
625
  redirectIfNeeded();
440
626
  ensureSafeOverview();
627
+ applyReadonlySystemInfoCard();
441
628
  filterNativeUsersPage();
442
629
  hideOfficialReserveSurfaces();
630
+ applyEndUserDatapointPolicy();
631
+ applyPrivilegedIframePolicy();
632
+ scrubInheritedUiAdminSession();
443
633
  ensureReserveObserver();
444
634
  suppressPermissionErrors();
445
635
  window.NEXOWATT_EOS_ACCOUNT_MANAGEMENT?.refreshEntry?.();
@@ -491,6 +681,10 @@
491
681
  if (state.role === 'installer') window.NEXOWATT_EOS_BASIC_SETTINGS?.open?.();
492
682
  return;
493
683
  }
684
+ if (state.role === 'enduser' && currentRoute() === 'tab-objects') {
685
+ const writeTarget = event.target?.closest?.('[data-eos-object-writable="1"],[data-eos-enduser-write-blocked="1"],button[title*="edit" i],button[title*="delete" i],button[aria-label*="edit" i],button[aria-label*="delete" i]');
686
+ if (writeTarget) { event.preventDefault(); event.stopImmediatePropagation(); return; }
687
+ }
494
688
  const target = event.target?.closest?.('[data-eos-admin-only-control="1"],[data-eos-system-settings-control="1"],[data-eos-role-tab],a[href*="#tab-"],a[href*="#/tab-"]');
495
689
  if (!target) return;
496
690
  if (target.matches('[data-eos-admin-only-control="1"]')) { event.preventDefault(); event.stopImmediatePropagation(); return; }
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v7101-nexowatt-native-shell-native-overview';
4
+ const VERSION = 'v7106-nexowatt-native-shell-native-overview';
5
5
  const previous = window.NEXOWATT_NATIVE_SHELL;
6
6
  if (previous?.version === VERSION) return;
7
7
  previous?.destroy?.();
@@ -1,6 +1,6 @@
1
1
  (() => {
2
2
  'use strict';
3
- const VERSION = 'v7101-scroll-standard-password';
3
+ const VERSION = 'v7106-scroll-standard-password';
4
4
  const ensureScroll = () => {
5
5
  const root = document.documentElement;
6
6
  const paper = document.getElementById('app-paper');
package/build/lib/web.js CHANGED
@@ -8,6 +8,7 @@ const node_util_1 = require("node:util");
8
8
  const node_path_1 = require("node:path");
9
9
  const node_os_1 = require("node:os");
10
10
  const node_stream_1 = require("node:stream");
11
+ const node_child_process_1 = require("node:child_process");
11
12
  const compression = require("compression");
12
13
  const mime_1 = require("mime");
13
14
  const node_zlib_1 = require("node:zlib");
@@ -1371,6 +1372,35 @@ class Web {
1371
1372
  this.adapter.log.info(`EOS installer basic settings updated by ${access.userId || 'unknown user'}`);
1372
1373
  await this.sendEosBasicSettings(req, res);
1373
1374
  }
1375
+
1376
+ getEosReadonlySystemInfo() {
1377
+ let npm = '--';
1378
+ try {
1379
+ const binary = process.platform === 'win32' ? 'npm.cmd' : 'npm';
1380
+ npm = (0, node_child_process_1.execFileSync)(binary, ['-v'], { encoding: 'utf8', timeout: 1500 }).trim() || '--';
1381
+ }
1382
+ catch {
1383
+ // ignore
1384
+ }
1385
+ return {
1386
+ platform: process.platform || '--',
1387
+ ramMb: Math.round((0, node_os_1.totalmem)() / 1024 / 1024),
1388
+ nodejs: process.version || '--',
1389
+ npm,
1390
+ active: true,
1391
+ };
1392
+ }
1393
+ async sendEosReadonlySystemInfo(req, res) {
1394
+ const access = await this.getEosRequestAccess(req);
1395
+ const authenticated = !this.settings.auth || !!access.userId;
1396
+ if (!authenticated) {
1397
+ res.status(401).json({ error: 'authenticationRequired' });
1398
+ return;
1399
+ }
1400
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1401
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
1402
+ res.status(200).json(this.getEosReadonlySystemInfo());
1403
+ }
1374
1404
  async sendEosSecuritySession(req, res) {
1375
1405
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1376
1406
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
@@ -1806,6 +1836,10 @@ class Web {
1806
1836
  this.server.app.get('/eos/security/status', sendSecuritySession);
1807
1837
  this.server.app.get('/nexowatt/security/session', sendSecuritySession);
1808
1838
  this.server.app.get('/nexowatt/security/context', sendSecuritySession);
1839
+ this.server.app.get('/nexowatt/readonly/system-info', (req, res) => { void this.sendEosReadonlySystemInfo(req, res).catch(e => {
1840
+ this.adapter.log.warn(`Cannot read EOS readonly system info: ${e instanceof Error ? e.message : e}`);
1841
+ res.status(500).json({ error: 'readonlySystemInfoFailed' });
1842
+ }); });
1809
1843
  this.server.app.get('/nexowatt/role-settings/basic', (req, res) => {
1810
1844
  void this.sendEosBasicSettings(req, res).catch(e => {
1811
1845
  this.adapter.log.warn(`Cannot read EOS basic settings: ${e instanceof Error ? e.message : e}`);
package/build/main.js CHANGED
@@ -1,7 +1,5 @@
1
1
  "use strict";
2
2
 
3
- const eosRoleSecurity = require("./lib/eosRoleSecurity");
4
- eosRoleSecurity.installHttpGuard();
5
3
  /**
6
4
  * Admin backend
7
5
  *
@@ -332,8 +330,6 @@ class Admin extends adapter_core_1.Adapter {
332
330
  * Is called when databases are connected and adapter received configuration.
333
331
  */
334
332
  onReady = async () => {
335
- eosRoleSecurity.attachAdapter(this);
336
- await eosRoleSecurity.enforceRoleAcls(this);
337
333
  const systemConfig = await this.getForeignObjectAsync('system.config');
338
334
  if (systemConfig) {
339
335
  systemConfig.native = systemConfig.native || {};
@@ -1272,14 +1268,9 @@ class Admin extends adapter_core_1.Adapter {
1272
1268
  return deny(socketClient, command, callback, 'technical diagnostics are installer/service-only');
1273
1269
  }
1274
1270
  if (command === 'changePassword') {
1275
- const targetRaw = String(args[0] || '').trim();
1276
- const targetUser = targetRaw.startsWith('system.user.') ? targetRaw : `system.user.${targetRaw}`;
1277
- const selfUser = String(userId || '');
1278
- const allowed = role === 'installer'
1279
- ? new Set([selfUser, 'system.user.installer', 'system.user.user', 'system.user.guest']).has(targetUser)
1280
- : targetUser === selfUser;
1281
- if (!allowed) return deny(socketClient, command, callback, 'password target outside EOS role scope');
1282
- return true;
1271
+ // Initial password activation has a separate, tightly scoped backend endpoint.
1272
+ // The normal Admin socket password editor remains Service/Admin-only.
1273
+ return deny(socketClient, command, callback, 'password administration is Service-only');
1283
1274
  }
1284
1275
  if (['addUser', 'delUser', 'addGroup', 'delGroup'].includes(command)) {
1285
1276
  return deny(socketClient, command, callback, 'account administration is Service-only');
@@ -1300,9 +1291,16 @@ class Admin extends adapter_core_1.Adapter {
1300
1291
  return deny(socketClient, command, callback, 'end users may only maintain Smart Home assignments');
1301
1292
  }
1302
1293
  }
1303
- if (['setState', 'delState', 'createState'].includes(command)
1304
- && /^(?:admin|backitup)\.\d+(?:\.|$)/.test(objectId)) {
1305
- return deny(socketClient, command, callback, 'internal Service reserve is Admin/Service-only');
1294
+ if (['setState', 'delState', 'createState'].includes(command)) {
1295
+ if (role === 'enduser') {
1296
+ return deny(socketClient, command, callback, 'end-user datapoints are read-only in EOS Admin');
1297
+ }
1298
+ if (objectId === `${this.namespace}.info.uiTabsVisible`) {
1299
+ return deny(socketClient, command, callback, 'navigation layout is Service-only');
1300
+ }
1301
+ if (/^(?:admin|backitup|xterm)\.\d+(?:\.|$)/.test(objectId)) {
1302
+ return deny(socketClient, command, callback, 'internal Service reserve is Admin/Service-only');
1303
+ }
1306
1304
  }
1307
1305
  return original(socketClient, command, callback, ...args);
1308
1306
  };