iobroker.eos-admin 7.9.95 → 7.9.96

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 (51) hide show
  1. package/INSTALL_TEST_V7.9.96.md +11 -0
  2. package/MERGE_UPDATE_README_V7.9.96.md +5 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +18 -23
  4. package/NexoWatt_EOS_Admin_EMS_OVERVIEW_MERGE_VALIDATION.md +44 -0
  5. package/PUBLISH_STABLE_V7.9.96.md +10 -0
  6. package/RC73_EOS_ADMIN_MERGE_VALIDATION.md +78 -0
  7. package/README_EMS_OVERVIEW_MERGE.md +45 -0
  8. package/README_STABILITY_V7.9.96.md +11 -0
  9. package/RELEASE_ACCEPTANCE_V7.9.96.md +10 -0
  10. package/RELEASE_NOTES_V7.9.96.md +12 -0
  11. package/VALIDATION_EMS_OVERVIEW_MERGE.md +35 -0
  12. package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
  13. package/adminWww/assets/index-D2ymscJA-v84.js +1 -1
  14. package/adminWww/css/eos-ems-overview.css +62 -0
  15. package/adminWww/css/nexowatt-native-shell.css +111 -13
  16. package/adminWww/index.html +15 -13
  17. package/adminWww/js/eos-assistant.js +1 -1
  18. package/adminWww/js/eos-ems-overview.js +140 -0
  19. package/adminWww/js/eos-role-bootstrap.js +4 -4
  20. package/adminWww/js/eos-role-ui.js +34 -38
  21. package/adminWww/js/nexowatt-native-shell.js +11 -6
  22. package/adminWww/static/js/nexowatt-stable-login.js +12 -12
  23. package/adminWww/static/js/nexowatt-stable-v96.js +82 -0
  24. package/io-package.json +10 -5
  25. package/package.json +38 -12
  26. package/tools/nexowatt-account-management-selftest.cjs +60 -0
  27. package/tools/nexowatt-apply-ems-overview.cjs +115 -0
  28. package/tools/nexowatt-clean-legacy-runtime.cjs +151 -0
  29. package/tools/nexowatt-ems-overview-selftest.cjs +35 -0
  30. package/tools/nexowatt-ensure-release-defaults.cjs +72 -0
  31. package/tools/nexowatt-generate-eos-admin-repo-entry.cjs +37 -0
  32. package/tools/nexowatt-generate-repo-entry.cjs +3 -0
  33. package/tools/nexowatt-internal-reserve-selftest.cjs +55 -0
  34. package/tools/nexowatt-login-layout-selftest.cjs +83 -0
  35. package/tools/nexowatt-merge-update-selftest.cjs +69 -0
  36. package/tools/nexowatt-modern-ui-selftest.cjs +49 -0
  37. package/tools/nexowatt-patch-repo.cjs +54 -0
  38. package/tools/nexowatt-publish-channel-guard.cjs +58 -0
  39. package/tools/nexowatt-release-defaults-selftest.cjs +29 -0
  40. package/tools/nexowatt-runtime-cleanup-selftest.cjs +47 -0
  41. package/tools/nexowatt-stable-v96-selftest.cjs +13 -0
  42. package/tools/nexowatt-sync-release-version.cjs +181 -0
  43. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +16 -0
  44. package/tools/nexowatt-validate-package.cjs +277 -0
  45. package/tools/nexowatt-version-sync-selftest.cjs +86 -0
  46. package/INSTALL_TEST_V7.9.95.md +0 -15
  47. package/NEXOWATT_EOS_STABLE_7.9.94.md +0 -16
  48. package/PUBLISH_STABLE_V7.9.95.md +0 -16
  49. package/README_STABILITY_V7.9.95.md +0 -12
  50. package/RELEASE_ACCEPTANCE_V7.9.95.md +0 -11
  51. package/RELEASE_NOTES_V7.9.95.md +0 -12
@@ -0,0 +1,62 @@
1
+ /* nexowatt-ems-overview-v1 – current runtime and future React build */
2
+ .eos-ems-overview-card {
3
+ --eos-ems-status-color: #48b9ff;
4
+ box-sizing: border-box;
5
+ flex: 1 1 720px;
6
+ min-width: min(100%, 420px);
7
+ max-width: 1180px;
8
+ min-height: 390px;
9
+ margin: 10px;
10
+ padding: 22px;
11
+ border: 1px solid color-mix(in srgb, var(--eos-ems-status-color) 38%, rgba(255,255,255,.08));
12
+ border-radius: 20px;
13
+ background: linear-gradient(145deg, rgba(4,24,35,.97), rgba(3,16,29,.98));
14
+ color: #ecf8ff;
15
+ box-shadow: 0 18px 55px rgba(0,0,0,.26), inset 0 0 0 1px rgba(255,255,255,.025);
16
+ overflow: hidden;
17
+ }
18
+ #eos-ems-overview-runtime { width: calc(100% - 32px); max-width: none; margin: 0 16px 20px; }
19
+ .eos-ems-overview-card * { box-sizing: border-box; }
20
+ .eos-ems-overview-header { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; }
21
+ .eos-ems-overview-header h2 { margin:5px 0 6px; font-size:clamp(1.2rem,2vw,1.65rem); color:#f5fbff; }
22
+ .eos-ems-overview-header p { margin:0; max-width:760px; color:#9eb2c2; line-height:1.45; }
23
+ .eos-ems-overview-eyebrow { color:#49e39e; font-size:.72rem; font-weight:900; letter-spacing:.13em; text-transform:uppercase; }
24
+ .eos-ems-overview-state { min-width:126px; display:grid; grid-template-columns:auto 1fr; align-items:center; gap:3px 8px; padding:9px 12px; border:1px solid rgba(255,255,255,.09); border-radius:999px; background:rgba(255,255,255,.035); }
25
+ .eos-ems-overview-state-dot { width:9px; height:9px; border-radius:50%; background:var(--eos-ems-status-color); box-shadow:0 0 15px var(--eos-ems-status-color); }
26
+ .eos-ems-overview-state strong { color:var(--eos-ems-status-color); }
27
+ .eos-ems-overview-state small { grid-column:2; color:#8095a6; }
28
+ .eos-ems-overview-metrics { display:grid; grid-template-columns:repeat(4,minmax(130px,1fr)); gap:10px; margin-top:19px; }
29
+ .eos-ems-overview-metric { min-width:0; padding:12px; border:1px solid rgba(255,255,255,.075); border-radius:14px; background:rgba(255,255,255,.025); }
30
+ .eos-ems-overview-metric span,.eos-ems-overview-metric small { display:block; color:#879cad; font-size:.72rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31
+ .eos-ems-overview-metric strong { display:block; margin:4px 0 2px; color:#f4fbff; font-size:1rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
32
+ .eos-ems-overview-budget { height:7px; margin:12px 0 10px; border-radius:999px; background:rgba(255,255,255,.06); overflow:hidden; }
33
+ .eos-ems-overview-budget span { display:block; height:100%; border-radius:inherit; background:linear-gradient(90deg,#01bc69,var(--eos-ems-status-color)); box-shadow:0 0 15px color-mix(in srgb,var(--eos-ems-status-color) 50%,transparent); transition:width .25s ease; }
34
+ .eos-ems-overview-tags { display:flex; flex-wrap:wrap; gap:7px; margin-bottom:14px; }
35
+ .eos-ems-overview-tags span { padding:5px 9px; border:1px solid rgba(72,185,255,.18); border-radius:999px; background:rgba(72,185,255,.07); color:#b9deef; font-size:.72rem; font-weight:750; }
36
+ .eos-ems-overview-tags span.is-warning { border-color:rgba(255,189,89,.3); background:rgba(255,189,89,.09); color:#ffd484; }
37
+ .eos-ems-overview-columns { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
38
+ .eos-ems-overview-panel { min-width:0; padding:14px; border:1px solid rgba(255,255,255,.065); border-radius:16px; background:rgba(0,0,0,.12); }
39
+ .eos-ems-overview-panel h3 { margin:0 0 10px; color:#dfeef6; font-size:.9rem; }
40
+ .eos-ems-overview-decision,.eos-ems-overview-event { position:relative; display:grid; gap:2px; margin-top:7px; padding:8px 9px 8px 12px; border-radius:10px; background:rgba(255,255,255,.025); overflow:hidden; }
41
+ .eos-ems-overview-decision::before,.eos-ems-overview-event::before { content:''; position:absolute; inset:0 auto 0 0; width:3px; background:#48b9ff; }
42
+ .eos-ems-overview-decision--warning::before,.eos-ems-overview-event--warning::before { background:#ffbd59; }
43
+ .eos-ems-overview-decision--error::before,.eos-ems-overview-event--error::before { background:#ff5f72; }
44
+ .eos-ems-overview-decision strong,.eos-ems-overview-event strong { color:#e7f4fa; font-size:.78rem; }
45
+ .eos-ems-overview-decision span,.eos-ems-overview-event span { color:#91a5b5; font-size:.72rem; line-height:1.35; }
46
+ .eos-ems-overview-decision small { color:#6fcba1; font-size:.68rem; }
47
+ .eos-ems-overview-event { grid-template-columns:64px 1fr; align-items:start; }
48
+ .eos-ems-overview-event time { color:#6f8799; font-size:.68rem; font-variant-numeric:tabular-nums; }
49
+ .eos-ems-overview-event div { display:grid; gap:2px; min-width:0; }
50
+ .eos-ems-overview-empty { color:#718797; font-size:.76rem; }
51
+ .eos-ems-overview-footer { display:flex; justify-content:space-between; gap:12px; margin-top:12px; color:#60798a; font-size:.67rem; }
52
+ .eos-ems-overview-footer a { color:#5ee0c2; font-weight:800; text-decoration:none; }
53
+ .eos-ems-overview-footer a:hover,.eos-ems-overview-footer a:focus-visible { color:#9af2dd; text-decoration:underline; }
54
+ @media (max-width: 1100px) { .eos-ems-overview-card { flex-basis:100%; max-width:none; } }
55
+ @media (max-width: 760px) {
56
+ #eos-ems-overview-runtime,.eos-ems-overview-card { min-width:0; width:calc(100% - 16px); margin:8px; padding:16px; }
57
+ .eos-ems-overview-header { display:grid; }
58
+ .eos-ems-overview-state { width:max-content; }
59
+ .eos-ems-overview-metrics { grid-template-columns:1fr 1fr; }
60
+ .eos-ems-overview-columns { grid-template-columns:1fr; }
61
+ .eos-ems-overview-footer { display:grid; }
62
+ }
@@ -75,8 +75,8 @@ html.eos-login main.MuiPaper-root::before {
75
75
  left: 50%;
76
76
  top: 50%;
77
77
  box-sizing: border-box;
78
- width: min(390px, calc(100vw - 24px));
79
- height: min(480px, calc(100vh - 24px));
78
+ width: min(520px, calc(100vw - 24px)) !important;
79
+ height: min(640px, calc(100dvh - 24px)) !important;
80
80
  transform: translate(-50%, -50%);
81
81
  border: 1px solid rgba(0, 229, 138, .34);
82
82
  border-radius: 26px;
@@ -90,13 +90,13 @@ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password) {
90
90
  position: relative;
91
91
  z-index: 1;
92
92
  box-sizing: border-box !important;
93
- width: min(334px, calc(100vw - 56px)) !important;
94
- height: min(424px, calc(100vh - 56px)) !important;
95
- min-height: 0 !important;
96
- max-width: none !important;
93
+ width: min(460px, calc(100vw - 56px)) !important;
94
+ height: auto !important;
95
+ min-height: 540px !important;
96
+ max-width: 460px !important;
97
97
  max-height: none !important;
98
- padding: 28px !important;
99
- overflow: auto !important;
98
+ padding: 34px 36px 32px !important;
99
+ overflow: visible !important;
100
100
  border-radius: 24px !important;
101
101
  color: var(--nx-text) !important;
102
102
  background: linear-gradient(180deg, rgba(14, 36, 56, .97), rgba(4, 15, 27, .96)) !important;
@@ -886,12 +886,12 @@ html.eos-login .eos-login-card-modern {
886
886
  /* Compact product login: the form remains the proven inner card while the
887
887
  decorative rear frame keeps a fixed, restrained footprint. */
888
888
  box-sizing: border-box !important;
889
- width: min(334px, calc(100vw - 56px)) !important;
890
- height: min(424px, calc(100vh - 56px)) !important;
891
- min-height: 0 !important;
892
- max-width: none !important;
889
+ width: min(460px, calc(100vw - 56px)) !important;
890
+ height: auto !important;
891
+ min-height: 540px !important;
892
+ max-width: 460px !important;
893
893
  max-height: none !important;
894
- overflow: auto !important;
894
+ overflow: visible !important;
895
895
  }
896
896
  .eos-login-role-selector {
897
897
  display: none !important;
@@ -1291,3 +1291,101 @@ html.eos-route-intro #app-paper [class*="card"] {
1291
1291
  .eos-account-management-entry { margin: 9px; padding: 19px; border-radius: 18px; }
1292
1292
  .eos-assist-header-root .eos-assist-panel { right: 8px !important; width: calc(100vw - 16px); }
1293
1293
  }
1294
+
1295
+
1296
+ /* NexoWatt EOS 7.9.96 – spacious login card without inner scrollbars. */
1297
+ html.eos-login,
1298
+ html.eos-login body {
1299
+ min-height: 100%;
1300
+ margin: 0 !important;
1301
+ overflow-x: hidden !important;
1302
+ overflow-y: auto !important;
1303
+ }
1304
+ html.eos-login #root {
1305
+ min-height: 100dvh;
1306
+ box-sizing: border-box;
1307
+ padding: 24px 12px;
1308
+ }
1309
+ html.eos-login main.MuiPaper-root {
1310
+ box-sizing: border-box !important;
1311
+ min-height: calc(100dvh - 48px) !important;
1312
+ padding: 28px 0 !important;
1313
+ overflow: visible !important;
1314
+ }
1315
+ html.eos-login main.MuiPaper-root::before {
1316
+ width: min(520px, calc(100vw - 24px)) !important;
1317
+ height: min(640px, calc(100dvh - 24px)) !important;
1318
+ min-height: 590px !important;
1319
+ }
1320
+ html.eos-login main.MuiPaper-root > .eos-login-card-modern,
1321
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),
1322
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password),
1323
+ html.eos-login [data-nw-login-card="true"] {
1324
+ box-sizing: border-box !important;
1325
+ width: min(460px, calc(100vw - 56px)) !important;
1326
+ min-width: 0 !important;
1327
+ min-height: 540px !important;
1328
+ height: auto !important;
1329
+ max-width: 460px !important;
1330
+ max-height: none !important;
1331
+ padding: 34px 36px 32px !important;
1332
+ overflow: visible !important;
1333
+ }
1334
+ html.eos-login main.MuiPaper-root > .eos-login-card-modern *,
1335
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username) *,
1336
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password) * {
1337
+ box-sizing: border-box;
1338
+ max-width: 100%;
1339
+ }
1340
+ html.eos-login .MuiAlert-root,
1341
+ html.eos-login [role="alert"],
1342
+ html.eos-login .eos-login-first-status,
1343
+ html.eos-login .eos-login-first-hint {
1344
+ position: static !important;
1345
+ width: 100% !important;
1346
+ max-width: 100% !important;
1347
+ max-height: none !important;
1348
+ overflow: visible !important;
1349
+ white-space: normal !important;
1350
+ overflow-wrap: anywhere !important;
1351
+ word-break: normal !important;
1352
+ flex-shrink: 0 !important;
1353
+ }
1354
+ html.eos-login input,
1355
+ html.eos-login button,
1356
+ html.eos-login form {
1357
+ max-width: 100% !important;
1358
+ }
1359
+ @media (max-width: 560px) {
1360
+ html.eos-login #root { padding: 10px 8px; }
1361
+ html.eos-login main.MuiPaper-root { min-height: calc(100dvh - 20px) !important; padding: 14px 0 !important; }
1362
+ html.eos-login main.MuiPaper-root::before {
1363
+ width: calc(100vw - 16px) !important;
1364
+ min-height: 0 !important;
1365
+ height: calc(100% - 4px) !important;
1366
+ border-radius: 20px !important;
1367
+ }
1368
+ html.eos-login main.MuiPaper-root > .eos-login-card-modern,
1369
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),
1370
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password),
1371
+ html.eos-login [data-nw-login-card="true"] {
1372
+ width: calc(100vw - 38px) !important;
1373
+ max-width: none !important;
1374
+ min-height: 0 !important;
1375
+ padding: 26px 22px !important;
1376
+ }
1377
+ }
1378
+ @media (max-height: 690px) {
1379
+ html.eos-login main.MuiPaper-root::before { min-height: 0 !important; height: calc(100% - 12px) !important; }
1380
+ html.eos-login main.MuiPaper-root > .eos-login-card-modern,
1381
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),
1382
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password),
1383
+ html.eos-login [data-nw-login-card="true"] { min-height: 0 !important; }
1384
+ }
1385
+
1386
+
1387
+ /* 7.9.96: native Intro owns the overview for every EOS role. */
1388
+ #eos-role-safe-overview,
1389
+ .eos-role-safe-overview { display: none !important; }
1390
+ html.eos-safe-overview-active #app-paper > :not(.MuiSnackbar-root):not([role="presentation"]) { pointer-events: auto !important; }
1391
+ .eos-intro-admin-edit-hidden { display: none !important; }
@@ -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=91"></script>
11
+ <script src="./js/eos-manual-write-policy.js?v=96"></script>
12
12
  <script type="module" src="./assets/hostInit-v84.js?v=84"></script>
13
13
 
14
14
  <meta charset="utf-8" />
@@ -36,7 +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=91" />
39
+ <link rel="stylesheet" href="./css/nexowatt-native-shell.css?v=96" />
40
+ <link rel="stylesheet" href="./css/eos-ems-overview.css?v=96" />
40
41
  <link
41
42
  rel="manifest"
42
43
  href="manifest.json"
@@ -158,20 +159,21 @@
158
159
  </script>
159
160
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js">
160
161
  <link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js">
161
- <script defer src="./js/eos-role-bootstrap.js?v=91" data-eos-entry="./assets/index-CQZugZ1z-v84.js?v=84"></script>
162
- <script defer src="./js/eos-policy-client.js?v=91"></script>
163
- <script defer src="./js/eos-branding-sanitizer.js?v=91"></script>
164
- <script defer src="./js/nexowatt-native-shell.js?v=91"></script>
165
- <script defer src="./js/eos-native-security.js?v=91"></script>
166
- <script defer src="./js/eos-basic-settings.js?v=91"></script>
167
- <script defer src="./js/eos-role-ui.js?v=91"></script>
168
- <script defer src="./js/eos-account-management.js?v=91"></script>
169
- <script defer src="./js/eos-assistant.js?v=91"></script>
162
+ <script defer src="./js/eos-role-bootstrap.js?v=96" data-eos-entry="./assets/index-CQZugZ1z-v84.js?v=84"></script>
163
+ <script defer src="./js/eos-policy-client.js?v=96"></script>
164
+ <script defer src="./js/eos-branding-sanitizer.js?v=96"></script>
165
+ <script defer src="./js/nexowatt-native-shell.js?v=96"></script>
166
+ <script defer src="./js/eos-native-security.js?v=96"></script>
167
+ <script defer src="./js/eos-basic-settings.js?v=96"></script>
168
+ <script defer src="./js/eos-role-ui.js?v=96"></script>
169
+ <script defer src="./js/eos-ems-overview.js?v=96"></script>
170
+ <script defer src="./js/eos-account-management.js?v=96"></script>
171
+ <script defer src="./js/eos-assistant.js?v=96"></script>
170
172
  </head>
171
173
  <body>
172
174
  <noscript>Für NexoWatt EOS muss JavaScript aktiviert sein.</noscript>
173
175
  <div id="root"></div>
174
- <script defer src="static/js/nexowatt-stable-login.js?v=95"></script>
175
- <script defer src="static/js/nexowatt-stable-v95.js?v=95"></script>
176
+ <script defer src="static/js/nexowatt-stable-login.js?v=96"></script>
177
+ <script defer src="static/js/nexowatt-stable-v96.js?v=96"></script>
176
178
  </body>
177
179
  </html>
@@ -1,6 +1,6 @@
1
1
  (() => {
2
2
  'use strict';
3
- // NexoWatt EOS stable 7.9.95: EOS Assist remains disabled until the
3
+ // NexoWatt EOS stable 7.9.96: EOS Assist remains disabled until the
4
4
  // read-only system assistant is rebuilt and accepted separately.
5
5
  window.NEXOWATT_EOS_ASSIST_DISABLED = true;
6
6
  document.documentElement.classList.add('eos-assist-disabled');
@@ -0,0 +1,140 @@
1
+ (() => {
2
+ 'use strict';
3
+ const VERSION = 'v96-nexowatt-ems-overview-runtime';
4
+ const CONTRACT = 'nexowatt-ems-overview-v1';
5
+ const ROOT_ID = 'eos-ems-overview-runtime';
6
+ const POLL_MS = 5000;
7
+ const STALE_MS = 20000;
8
+ const COLORS = { ok: '#01bc69', info: '#48b9ff', warning: '#ffbd59', error: '#ff5f72' };
9
+ let timer = 0;
10
+ let inFlight = false;
11
+ let destroyed = false;
12
+
13
+ const esc = value => String(value ?? '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[character]));
14
+ const text = (value, fallback = '') => String(value ?? fallback).replace(/\s+/g, ' ').trim() || fallback;
15
+ const num = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
16
+ const parse = (value, fallback) => {
17
+ if (value && typeof value === 'object') return value;
18
+ try { return JSON.parse(String(value ?? '')); } catch (_) { return fallback; }
19
+ };
20
+ const severity = value => {
21
+ const normalized = String(value || '').toLowerCase();
22
+ if (normalized === 'error') return 'error';
23
+ if (normalized === 'warning' || normalized === 'warn') return 'warning';
24
+ if (normalized === 'ok') return 'ok';
25
+ return 'info';
26
+ };
27
+ const currentTab = () => String(location.hash || '').match(/tab-[a-z0-9_-]+/i)?.[0]?.toLowerCase() || 'tab-intro';
28
+ const currentRole = () => String(window.NEXOWATT_EOS_ACCESS_ROLE || 'admin').toLowerCase();
29
+ const technical = () => ['admin', 'service', 'installer', 'installateur'].includes(currentRole());
30
+ const formatPower = value => {
31
+ const watts = num(value); const absolute = Math.abs(watts); const sign = watts < 0 ? '−' : '';
32
+ return absolute >= 1000 ? `${sign}${(absolute / 1000).toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 2 })} kW` : `${sign}${Math.round(absolute).toLocaleString('de-DE')} W`;
33
+ };
34
+ const formatAge = timestamp => {
35
+ const value = num(timestamp); if (!value) return 'noch kein Regeltick';
36
+ const seconds = Math.max(0, Math.round((Date.now() - value) / 1000));
37
+ if (seconds < 2) return 'gerade eben';
38
+ if (seconds < 60) return `vor ${seconds} s`;
39
+ return `vor ${Math.round(seconds / 60)} min`;
40
+ };
41
+ const formatClock = timestamp => {
42
+ const value = num(timestamp); if (!value) return '—';
43
+ try { return new Date(value).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch (_) { return '—'; }
44
+ };
45
+ const getSocket = () => window.NEXOWATT_EOS_ADMIN_SOCKET || window.NEXOWATT_EOS_SOCKET || null;
46
+ const stateVal = (states, id) => states?.[id]?.val;
47
+
48
+ const normalizeInstances = (states, aliveStates) => {
49
+ const prefixes = new Set();
50
+ Object.keys(states || {}).forEach(id => { const match = id.match(/^(nexowatt-ui\.\d+)\.info\.adminOverview\./); if (match) prefixes.add(match[1]); });
51
+ return Array.from(prefixes).map(instance => {
52
+ const summaryId = `${instance}.info.adminOverview.summaryJson`;
53
+ const summary = parse(stateVal(states, summaryId), {});
54
+ const events = parse(stateVal(states, `${instance}.info.adminOverview.eventsJson`), []);
55
+ const updatedAt = Math.max(num(summary.updatedAt), num(summary.generatedAt), num(stateVal(states, `${instance}.info.adminOverview.updatedAt`)), num(states?.[summaryId]?.ts));
56
+ const rawAlive = stateVal(aliveStates, `system.adapter.${instance}.alive`);
57
+ const alive = rawAlive == null ? null : (rawAlive === true || rawAlive === 1 || ['true', '1', 'online', 'active'].includes(String(rawAlive).toLowerCase()));
58
+ return { instance, alive, summary: { ...summary, status: severity(summary.status || stateVal(states, `${instance}.info.adminOverview.status`)), headline: text(summary.headline || stateVal(states, `${instance}.info.adminOverview.headline`), 'EMS-Diagnose wird aufgebaut'), reason: text(summary.reason || stateVal(states, `${instance}.info.adminOverview.reason`), ''), binding: text(summary.binding || stateVal(states, `${instance}.info.adminOverview.binding`), 'none') }, events: Array.isArray(events) ? events : [], updatedAt };
59
+ }).sort((a, b) => Number(b.alive === true) - Number(a.alive === true) || b.updatedAt - a.updatedAt || a.instance.localeCompare(b.instance));
60
+ };
61
+
62
+ const ensureRoot = () => {
63
+ const reactCard = document.querySelector(`[data-nexowatt-contract="${CONTRACT}"]:not(#${ROOT_ID})`);
64
+ if (reactCard) { document.getElementById(ROOT_ID)?.remove(); return null; }
65
+ const paper = document.getElementById('app-paper');
66
+ if (!paper || currentTab() !== 'tab-intro') return null;
67
+ let root = document.getElementById(ROOT_ID);
68
+ if (!root) {
69
+ root = document.createElement('section');
70
+ root.id = ROOT_ID;
71
+ root.className = 'eos-ems-overview-card';
72
+ root.dataset.nexowattContract = CONTRACT;
73
+ }
74
+ const hero = document.getElementById('eos-native-overview-hero');
75
+ if (hero?.parentElement === paper) paper.insertBefore(root, hero.nextSibling); else paper.insertBefore(root, paper.firstChild || null);
76
+ return root;
77
+ };
78
+
79
+ const unavailable = message => {
80
+ const root = ensureRoot(); if (!root) return;
81
+ root.style.setProperty('--eos-ems-status-color', COLORS.info);
82
+ root.innerHTML = `<header class="eos-ems-overview-header"><div><span class="eos-ems-overview-eyebrow">NexoWatt EMS</span><h2>Live-Diagnose</h2><p>${esc(message)}</p></div><div class="eos-ems-overview-state"><span class="eos-ems-overview-state-dot"></span><strong>Nicht verfügbar</strong><small>read-only</small></div></header><footer class="eos-ems-overview-footer"><span>Die Kachel liest ausschließlich info.adminOverview.*.</span><span>Keine EMS-Regelung wurde verändert.</span></footer>`;
83
+ };
84
+
85
+ const fallbackDecisions = summary => {
86
+ const result = []; const charging = summary.charging || {}; const storage = summary.storage || {};
87
+ if (charging.available) result.push({ severity: num(charging.faultCount) > 0 ? 'error' : num(charging.waitingCount) > 0 ? 'info' : 'ok', title: `${num(charging.activeCount)} lädt · ${num(charging.waitingCount)} wartet · ${num(charging.faultCount)} gestört`, reason: text(charging.limiterText || charging.status, 'Lademanagement aktiv'), details: `Ist ${formatPower(charging.actualW)} · Soll ${formatPower(charging.targetW)}` });
88
+ if (storage.available) result.push({ severity: storage.writeOk === false ? 'error' : 'ok', title: `Speicher ${text(storage.topology, 'aktiv')}${storage.socPct != null ? ` · SoC ${Math.round(num(storage.socPct))} %` : ''}`, reason: text(storage.reason, 'Speicherregelung aktiv'), details: `Ist ${formatPower(storage.actualW)} · Soll ${formatPower(storage.targetW)}` });
89
+ return result;
90
+ };
91
+
92
+ const render = data => {
93
+ const root = ensureRoot(); if (!root) return;
94
+ const summary = data.summary || {}; const budget = summary.budget || {}; const charging = summary.charging || {}; const storage = summary.storage || {}; const ems = summary.ems || {}; const para14a = summary.para14a || {}; const tariff = summary.tariff || {}; const forecast = summary.forecast || {}; const peak = summary.peakShaving || {};
95
+ const stale = data.alive === false || !data.updatedAt || Date.now() - data.updatedAt > STALE_MS;
96
+ const status = stale ? 'error' : severity(summary.status); const statusColor = COLORS[status]; root.style.setProperty('--eos-ems-status-color', statusColor);
97
+ const totalBudget = Math.max(0, num(budget.totalW)); const remaining = Math.max(0, num(budget.remainingW)); const percent = totalBudget > 0 ? Math.max(0, Math.min(100, remaining / totalBudget * 100)) : 0;
98
+ const isTechnical = technical(); const decisions = (Array.isArray(summary.currentDecisions) ? summary.currentDecisions : fallbackDecisions(summary)).slice(0, isTechnical ? 5 : 2); const events = isTechnical ? data.events.slice(0, 6) : [];
99
+ const tags = [];
100
+ if (text(summary.binding, 'none') !== 'none') tags.push({ label: `Bindend: ${text(budget.bindingText || summary.binding)}`, warning: true });
101
+ if (para14a.communicationFallbackActive) tags.push({ label: `§14a-Fallback ${formatPower(para14a.fallbackCapW)}`, warning: true }); else if (para14a.binding) tags.push({ label: '§14a begrenzt', warning: true });
102
+ if (peak.active) tags.push({ label: `Peak-Shaving: ${text(peak.status, 'aktiv')}`, warning: true });
103
+ if (tariff.available) tags.push({ label: `Tarif: ${text(tariff.state, 'aktiv')}${tariff.priceEurPerKwh != null ? ` · ${num(tariff.priceEurPerKwh).toFixed(3)} €/kWh` : ''}` });
104
+ if (forecast.available) tags.push({ label: `Prognose: ${text(forecast.source, 'aktiv')}${forecast.fresh === false ? ' · veraltet' : ''}`, warning: forecast.fresh === false });
105
+ if (ems.safetyActive || ems.safetyEmergencyStop || ems.safetyValid === false) tags.push({ label: 'EOS Safety aktiv', warning: true });
106
+ const detailsPort = Math.max(1, num(summary.details?.port, 8188)); const detailsPath = text(summary.details?.path, '/ems-apps.html?tab=status'); const detailsUrl = `${location.protocol}//${location.hostname}:${detailsPort}${detailsPath.startsWith('/') ? detailsPath : `/${detailsPath}`}`;
107
+ const metrics = `<div class="eos-ems-overview-metric"><span>EMS-Budget</span><strong>${totalBudget > 0 ? esc(formatPower(totalBudget)) : 'nicht begrenzt'}</strong><small>Rest ${esc(formatPower(remaining))}</small></div><div class="eos-ems-overview-metric"><span>Lademanagement</span><strong>${esc(formatPower(charging.actualW))} / ${esc(formatPower(charging.targetW))}</strong><small>${num(charging.activeCount)} lädt · ${num(charging.waitingCount)} wartet</small></div>${storage.available ? `<div class="eos-ems-overview-metric"><span>Speicher ${esc(text(storage.topology))}</span><strong>${esc(formatPower(storage.actualW))} / ${esc(formatPower(storage.targetW))}</strong><small>${storage.socPct != null ? `SoC ${Math.round(num(storage.socPct))} %` : esc(text(storage.reason, 'aktiv'))}</small></div>` : `<div class="eos-ems-overview-metric"><span>PV-Budget</span><strong>${esc(formatPower(budget.pvBudgetW))}</strong><small>PV-Rest ${esc(formatPower(budget.remainingPvW))}</small></div>`}<div class="eos-ems-overview-metric"><span>Letzter Regeltick</span><strong>${esc(formatAge(ems.lastTickTs || data.updatedAt))}</strong><small>${isTechnical ? `Zyklus ${num(ems.cycleMs)} ms` : esc(text(ems.decision, 'EMS aktiv'))}</small></div>`;
108
+ const decisionsHtml = decisions.length ? decisions.map(item => `<div class="eos-ems-overview-decision eos-ems-overview-decision--${severity(item.severity)}"><strong>${esc(text(item.title, 'EMS-Entscheidung'))}</strong><span>${esc(text(item.reason, 'Keine zusätzliche Begrenzung'))}</span>${isTechnical && item.details ? `<small>${esc(item.details)}</small>` : ''}</div>`).join('') : '<div class="eos-ems-overview-empty">Keine aktiven Regelentscheidungen. EOS arbeitet mit den aktuellen Messwerten.</div>';
109
+ const eventsHtml = isTechnical ? (events.length ? events.map(item => `<div class="eos-ems-overview-event eos-ems-overview-event--${severity(item.severity)}"><time>${esc(formatClock(item.ts))}</time><div><strong>${esc(text(item.title, 'EMS'))}</strong><span>${esc(text(item.message || item.reason, 'Zustand aktualisiert'))}</span></div></div>`).join('') : '<div class="eos-ems-overview-empty">Noch keine Zustandsänderung im begrenzten Ereignispuffer.</div>') : `<div class="eos-ems-overview-decision eos-ems-overview-decision--info"><strong>${esc(text(summary.headline, 'EMS aktiv'))}</strong><span>${esc(text(summary.reason, 'EOS optimiert die Anlage innerhalb aller Sicherheitsgrenzen.'))}</span></div>`;
110
+ root.innerHTML = `<header class="eos-ems-overview-header"><div><span class="eos-ems-overview-eyebrow">NexoWatt EMS · Live-Diagnose</span><h2>${esc(stale ? 'EMS-Diagnose ist nicht aktuell' : text(summary.headline, 'EMS arbeitet normal'))}</h2><p>${esc(stale ? 'Adapter oder Diagnosewerte sind älter als 20 Sekunden. Die Werte werden als veraltet markiert.' : text(summary.reason, 'EMS arbeitet innerhalb aller aktiven Grenzen.'))}</p></div><div class="eos-ems-overview-state"><span class="eos-ems-overview-state-dot"></span><strong>${stale ? 'Offline / veraltet' : ({ok:'Normal',info:'Information',warning:'Begrenzt',error:'Störung'}[status])}</strong><small>${esc(data.instance)} · ${esc(formatAge(data.updatedAt))}</small></div></header><div class="eos-ems-overview-metrics">${metrics}</div>${totalBudget > 0 ? `<div class="eos-ems-overview-budget" title="${percent.toFixed(1)} % Restbudget"><span style="width:${percent}%"></span></div>` : ''}${tags.length ? `<div class="eos-ems-overview-tags">${tags.map(tag => `<span class="${tag.warning ? 'is-warning' : ''}">${esc(tag.label)}</span>`).join('')}</div>` : ''}<div class="eos-ems-overview-columns"><div class="eos-ems-overview-panel"><h3>Aktuelle Regelentscheidungen</h3>${decisionsHtml}</div><div class="eos-ems-overview-panel"><h3>${isTechnical ? 'Letzte EMS-Ereignisse' : 'Systemerklärung'}</h3>${eventsHtml}</div></div><footer class="eos-ems-overview-footer"><span>Diagnose ist rein lesend. Keine Kachel besitzt Schreibhoheit.</span><a href="${esc(detailsUrl)}" target="_blank" rel="noreferrer">Vollständige EMS-Diagnose öffnen ↗</a></footer>`;
111
+ };
112
+
113
+ const load = async () => {
114
+ if (destroyed || inFlight || document.visibilityState === 'hidden' || currentTab() !== 'tab-intro') return;
115
+ const socket = getSocket();
116
+ if (!socket?.getForeignStates) { unavailable('Die Admin-Verbindung wird noch aufgebaut.'); return; }
117
+ inFlight = true;
118
+ try {
119
+ const [states, alive] = await Promise.all([socket.getForeignStates('nexowatt-ui.*.info.adminOverview.*'), socket.getForeignStates('system.adapter.nexowatt-ui.*.alive')]);
120
+ const instances = normalizeInstances(states || {}, alive || {});
121
+ if (instances.length) render(instances[0]); else unavailable('NexoWatt UI ist nicht installiert, offline oder der Diagnosevertrag info.adminOverview.* fehlt.');
122
+ } catch (error) { unavailable(`EMS-Diagnose konnte nicht gelesen werden: ${text(error?.message || error, 'unbekannter Fehler')}`); }
123
+ finally { inFlight = false; }
124
+ };
125
+
126
+ const apply = () => {
127
+ document.getElementById('eos-role-safe-overview')?.remove();
128
+ document.documentElement.classList.remove('eos-safe-overview-active');
129
+ if (currentTab() !== 'tab-intro') { document.getElementById(ROOT_ID)?.remove(); return; }
130
+ ensureRoot(); void load();
131
+ };
132
+ const start = () => {
133
+ apply();
134
+ timer = window.setInterval(apply, POLL_MS);
135
+ window.addEventListener('hashchange', apply);
136
+ document.addEventListener('visibilitychange', apply);
137
+ };
138
+ document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, { once: true }) : start();
139
+ window.NEXOWATT_EOS_EMS_OVERVIEW = Object.freeze({ version: VERSION, contract: CONTRACT, refresh: apply, destroy() { destroyed = true; if (timer) clearInterval(timer); document.getElementById(ROOT_ID)?.remove(); } });
140
+ })();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v91-final-first-login-submit-guard';
4
+ const VERSION = 'v96-first-login-password-native-overview';
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;
@@ -172,7 +172,7 @@
172
172
  overlay.querySelector('.eos-first-login-submit').textContent = t.submit;
173
173
  overlay.querySelector('.eos-first-login-logout').textContent = t.logout;
174
174
  const form = overlay.querySelector('form');
175
- form.elements.username.value = claim.userName || 'guest';
175
+ form.elements.username.value = userName;
176
176
  const password = form.elements.password;
177
177
  const repeat = form.elements.passwordRepeat;
178
178
  const show = form.elements.show;
@@ -500,7 +500,7 @@
500
500
  const { username, password, card, submit } = loginElements();
501
501
  if (!username || !password || !card) return;
502
502
  card.classList.add('eos-login-card-modern');
503
- // v91 compact normal-login first activation: do not add role buttons or a second panel.
503
+ // v96 compact normal-login first activation: do not add role buttons or a second panel.
504
504
  // The account is entered in the existing Loginname field; a blank password is accepted only
505
505
  // after the backend confirms that this exact managed account is eligible.
506
506
  card.querySelectorAll('.eos-login-role-selector').forEach(element => element.remove());
@@ -631,7 +631,7 @@
631
631
 
632
632
  const contextUrl = new URL('nexowatt/security/context', base).href;
633
633
  const controller = new AbortController();
634
- const timeout = window.setTimeout(() => controller.abort(), 3500);
634
+ const timeout = window.setTimeout(() => controller.abort(), 8000);
635
635
 
636
636
  window.NEXOWATT_EOS_BOOTSTRAP_READY = fetch(contextUrl, {
637
637
  credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' }, signal: controller.signal,
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v91-final-clickable-overview-live-reserve-filter';
4
+ const VERSION = 'v96-native-overview-ems-live-reserve-filter';
5
5
  window.NEXOWATT_EOS_ROLE_UI_VERSION = VERSION;
6
6
 
7
7
  const state = {
@@ -251,47 +251,43 @@
251
251
  if (/permissionerror|cannot get data/i.test(node.textContent || '')) node.classList.add('eos-role-hidden');
252
252
  });
253
253
  };
254
- const ensureSafeOverview = () => safe(() => {
255
- if (isLoginView() || state.role === 'admin' || state.role === 'unknown' || currentRoute() !== 'tab-intro') {
256
- removeSafeOverview();
257
- return;
254
+ const updateNativeOverviewRole = () => {
255
+ if (isLoginView() || currentRoute() !== 'tab-intro') return;
256
+ const config = state.role === 'admin'
257
+ ? { label: 'Admin / Service', text: 'Systemstatus, Module, Dienste und Anlagenzugänge in einer modernen Serviceübersicht.' }
258
+ : state.role === 'installer'
259
+ ? { label: 'Installateur', text: 'Inbetriebnahme, Fehlersuche, Geräte- und EMS-Diagnose – mit deinen freigegebenen Installateurrechten.' }
260
+ : { label: 'Endkunde', text: 'Energie, Laden, Gebäude und die aktuellen EMS-Entscheidungen auf einen Blick.' };
261
+ const hero = document.getElementById('eos-native-overview-hero');
262
+ const description = hero?.querySelector('p');
263
+ const role = hero?.querySelector('.eos-overview-role');
264
+ if (description) description.textContent = config.text;
265
+ if (role) {
266
+ role.innerHTML = '<span class="eos-overview-status-dot"></span>';
267
+ role.append(document.createTextNode(config.label));
268
+ role.setAttribute('data-nexowatt-overview-role', state.role);
258
269
  }
270
+ };
271
+ const hideIntroEditControls = () => {
259
272
  const paper = appPaper();
260
273
  if (!paper) return;
261
- document.documentElement.classList.add('eos-safe-overview-active');
262
- suppressPermissionErrors();
263
- let overview = document.getElementById('eos-role-safe-overview');
264
- if (!overview) {
265
- overview = document.createElement('section');
266
- overview.id = 'eos-role-safe-overview';
267
- overview.className = 'eos-role-safe-overview';
268
- paper.appendChild(overview);
269
- }
270
- const installer = state.role === 'installer';
271
- const actions = actionDefinitions(state.role).filter(([, , tab]) => tab === 'basic-settings' || isRouteAllowed(state.role, tab));
272
- overview.innerHTML = `
273
- <header class="eos-overview-hero eos-role-overview-hero">
274
- <div><span class="eos-overview-eyebrow">NexoWatt EOS</span><h1>Übersicht</h1><p>${installer ? 'Inbetriebnahme, Fehlersuche und Anlagenkonfiguration – passend zu deinen Installateurrechten.' : 'Deine freigegebenen Energie-, Smart-Home- und Sicherungsfunktionen.'}</p></div>
275
- <div class="eos-overview-role"><span class="eos-overview-status-dot"></span>${installer ? 'Installateur' : 'Gast / Endkunde'}</div>
276
- </header>
277
- <div class="eos-overview-grid">${actions.map(([label, description, tab, icon]) => `
278
- <a class="eos-overview-action" data-eos-role-tab="${tab}" href="${tab === 'basic-settings' ? '#tab-intro' : `#${tab}`}">
279
- <span class="eos-overview-action-icon" aria-hidden="true">${iconFor(icon)}</span>
280
- <span><strong>${label}</strong><small>${description}</small></span><i aria-hidden="true">›</i>
281
- </a>`).join('')}</div>`;
282
- overview.querySelectorAll('a[data-eos-role-tab]').forEach(link => {
283
- link.addEventListener('click', event => {
284
- event.preventDefault();
285
- event.stopPropagation();
286
- const targetTab = link.getAttribute('data-eos-role-tab') || '';
287
- if (targetTab) navigateToTab(targetTab, link.textContent || '');
288
- });
289
- link.addEventListener('keydown', event => {
290
- if (event.key !== 'Enter' && event.key !== ' ') return;
291
- event.preventDefault();
292
- navigateToTab(link.getAttribute('data-eos-role-tab') || '', link.textContent || '');
293
- });
274
+ const admin = state.role === 'admin';
275
+ paper.querySelectorAll(':scope > .MuiFab-root,:scope > button.MuiFab-root').forEach(button => {
276
+ button.classList.toggle('eos-intro-admin-edit-hidden', !admin);
277
+ if (!admin) button.setAttribute('aria-hidden', 'true'); else button.removeAttribute('aria-hidden');
294
278
  });
279
+ };
280
+ const ensureSafeOverview = () => safe(() => {
281
+ // 7.9.96: never cover the real Admin Intro with a second tile surface.
282
+ // Installer and end-user use the native card component; security remains
283
+ // enforced by route/instance filters and backend permissions.
284
+ removeSafeOverview();
285
+ if (isLoginView() || state.role === 'unknown' || currentRoute() !== 'tab-intro') return;
286
+ suppressPermissionErrors();
287
+ updateNativeOverviewRole();
288
+ hideIntroEditControls();
289
+ window.NEXOWATT_NATIVE_SHELL?.refresh?.();
290
+ window.NEXOWATT_EOS_EMS_OVERVIEW?.refresh?.();
295
291
  });
296
292
 
297
293
  const hideOfficialReserveSurfaces = () => safe(() => {
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v91-nexowatt-native-shell-modern-overview';
4
+ const VERSION = 'v96-nexowatt-native-shell-native-overview';
5
5
  const previous = window.NEXOWATT_NATIVE_SHELL;
6
6
  if (previous?.version === VERSION) return;
7
7
  previous?.destroy?.();
@@ -148,21 +148,26 @@
148
148
 
149
149
  const ensureModernOverview = () => {
150
150
  const paper = document.getElementById('app-paper');
151
- const role = window.NEXOWATT_EOS_ACCESS_ROLE || 'unknown';
152
- if (!paper || isLogin() || currentTab() !== 'tab-intro' || role !== 'admin') {
151
+ const role = String(window.NEXOWATT_EOS_ACCESS_ROLE || 'unknown').toLowerCase();
152
+ if (!paper || isLogin() || currentTab() !== 'tab-intro' || role === 'unknown') {
153
153
  document.getElementById('eos-native-overview-hero')?.remove();
154
154
  return;
155
155
  }
156
+ const config = role === 'admin'
157
+ ? { label: 'Admin / Service', text: 'Systemstatus, Module, Dienste und Anlagenzugänge in einer modernen Serviceübersicht.' }
158
+ : role === 'installer' || role === 'installateur'
159
+ ? { label: 'Installateur', text: 'Inbetriebnahme, Fehlersuche, Geräte- und EMS-Diagnose – mit deinen freigegebenen Installateurrechten.' }
160
+ : { label: 'Endkunde', text: 'Energie, Laden, Gebäude und die aktuellen EMS-Entscheidungen auf einen Blick.' };
156
161
  let hero = document.getElementById('eos-native-overview-hero');
157
162
  if (!hero) {
158
163
  hero = document.createElement('section');
159
164
  hero.id = 'eos-native-overview-hero';
160
165
  hero.className = 'eos-overview-hero eos-native-overview-hero';
161
- hero.innerHTML = `
162
- <div><span class="eos-overview-eyebrow">NexoWatt EOS</span><h1>Übersicht</h1><p>Systemstatus, Module, Dienste und Anlagenzugänge in einer modernen Serviceübersicht.</p></div>
163
- <div class="eos-overview-role"><span class="eos-overview-status-dot"></span>Admin / Service</div>`;
164
166
  paper.insertBefore(hero, paper.firstChild || null);
165
167
  }
168
+ hero.innerHTML = `
169
+ <div><span class="eos-overview-eyebrow">NexoWatt EOS</span><h1>Übersicht</h1><p>${config.text}</p></div>
170
+ <div class="eos-overview-role" data-nexowatt-overview-role="${role}"><span class="eos-overview-status-dot"></span>${config.label}</div>`;
166
171
  };
167
172
 
168
173
  const apply = () => {