iobroker.eos-admin 7.9.98 → 7.9.100

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 (57) hide show
  1. package/INSTALL_TEST_V7.9.100.md +11 -0
  2. package/MERGE_UPDATE_README_V7.9.100.md +5 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +20 -20
  4. package/PUBLISH_STABLE_V7.9.100.md +15 -0
  5. package/README.md +6 -16
  6. package/README_STABILITY_V7.9.100.md +3 -0
  7. package/RELEASE_ACCEPTANCE_V7.9.100.md +11 -0
  8. package/RELEASE_NOTES_V7.9.100.md +11 -0
  9. package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
  10. package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
  11. package/adminWww/css/eos-auto-update.css +4 -0
  12. package/adminWww/css/eos-ems-overview.css +3 -5
  13. package/adminWww/css/nexowatt-native-shell.css +26 -112
  14. package/adminWww/index.html +21 -18
  15. package/adminWww/js/eos-account-management.js +19 -28
  16. package/adminWww/js/eos-assistant.js +1 -1
  17. package/adminWww/js/eos-auto-update.js +78 -0
  18. package/adminWww/js/eos-ems-overview.js +2 -3
  19. package/adminWww/js/eos-release-watch.js +77 -0
  20. package/adminWww/js/eos-role-bootstrap.js +11 -8
  21. package/adminWww/js/eos-role-ui.js +2 -2
  22. package/adminWww/js/nexowatt-native-shell.js +1 -1
  23. package/adminWww/static/js/nexowatt-stable-login.js +3 -3
  24. package/adminWww/static/js/{nexowatt-stable-v98.js → nexowatt-stable-v100.js} +2 -2
  25. package/build/lib/eosAutoUpdate.js +43 -0
  26. package/build/lib/eosPassword.js +62 -0
  27. package/build/lib/eosRequestSecurity.js +31 -0
  28. package/build/lib/web.js +194 -151
  29. package/build/main.js +2 -1
  30. package/io-package.json +7 -11
  31. package/package.json +23 -13
  32. package/src/lib/eosAutoUpdate.ts +320 -0
  33. package/tools/nexowatt-account-management-selftest.cjs +19 -15
  34. package/tools/nexowatt-apply-ems-overview.cjs +2 -4
  35. package/tools/nexowatt-auto-update-selftest.cjs +34 -0
  36. package/tools/nexowatt-cache-update-selftest.cjs +12 -0
  37. package/tools/nexowatt-clean-legacy-runtime.cjs +1 -1
  38. package/tools/nexowatt-ems-overview-selftest.cjs +1 -1
  39. package/tools/nexowatt-login-layout-selftest.cjs +11 -11
  40. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  41. package/tools/nexowatt-modern-ui-selftest.cjs +2 -2
  42. package/tools/nexowatt-password-write-selftest.cjs +42 -44
  43. package/tools/nexowatt-request-security-selftest.cjs +11 -0
  44. package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -6
  45. package/tools/nexowatt-stable-v100-selftest.cjs +27 -0
  46. package/tools/nexowatt-sync-release-version.cjs +1 -42
  47. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +3 -1
  48. package/tools/nexowatt-validate-package.cjs +23 -23
  49. package/tools/nexowatt-version-sync-selftest.cjs +1 -18
  50. package/INSTALL_TEST_V7.9.98.md +0 -39
  51. package/MERGE_UPDATE_README_V7.9.98.md +0 -29
  52. package/PUBLISH_STABLE_V7.9.98.md +0 -33
  53. package/README_STABILITY_V7.9.98.md +0 -22
  54. package/RELEASE_ACCEPTANCE_V7.9.98.md +0 -48
  55. package/RELEASE_NOTES_V7.9.98.md +0 -32
  56. package/tools/nexowatt-account-password-flow-selftest.cjs +0 -186
  57. package/tools/nexowatt-stable-v98-selftest.cjs +0 -14
@@ -0,0 +1,4 @@
1
+ /* NexoWatt EOS native stable auto-update card */
2
+ .eos-nexowatt-auto-update{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:28px;width:calc(100% - 32px);margin:0 16px 14px;padding:22px 24px;border:1px solid rgba(0,229,138,.28);border-radius:20px;background:linear-gradient(120deg,rgba(0,71,66,.62),rgba(4,25,39,.96) 52%,rgba(4,21,34,.98));color:#effbff;box-shadow:0 16px 36px rgba(0,0,0,.18)}
3
+ .eos-nexowatt-auto-update[data-enabled="false"]{border-color:rgba(117,145,160,.34);background:linear-gradient(120deg,rgba(31,54,65,.72),rgba(4,21,34,.98))}
4
+ .eos-nau-copy{min-width:0;flex:1}.eos-nau-eyebrow{display:block;margin-bottom:5px;color:#00e58a;font-size:.72rem;font-weight:900;letter-spacing:.13em;text-transform:uppercase}.eos-nau-copy h2{margin:0 0 6px;font-size:1.35rem;line-height:1.2}.eos-nau-copy p{max-width:960px;margin:0;color:#a9c2d0;font-size:.82rem;line-height:1.55}.eos-nau-meta{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:13px;color:#84a8ba;font-size:.72rem}.eos-nau-meta strong{color:#d9f6ee}.eos-nau-adapters{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.eos-nau-adapters span{padding:5px 9px;border:1px solid rgba(0,229,138,.24);border-radius:999px;background:rgba(0,229,138,.08);color:#b9f6db;font-size:.7rem;font-weight:800}.eos-nau-error{margin-top:10px;padding:8px 10px;border:1px solid rgba(255,183,77,.38);border-radius:9px;background:rgba(255,152,0,.09);color:#ffd18d;font-size:.72rem}.eos-nau-switch{display:grid;grid-template-columns:50px auto;grid-template-rows:auto auto;align-items:center;column-gap:10px;min-width:160px;cursor:pointer}.eos-nau-switch input{position:absolute;opacity:0;pointer-events:none}.eos-nau-track{grid-row:1/3;position:relative;width:50px;height:28px;border:1px solid rgba(130,161,176,.55);border-radius:999px;background:#273e4c;transition:.2s}.eos-nau-thumb{position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#b6c8d1;box-shadow:0 3px 8px rgba(0,0,0,.28);transition:.2s}.eos-nau-switch input:checked+.eos-nau-track{border-color:#00e58a;background:rgba(0,229,138,.26)}.eos-nau-switch input:checked+.eos-nau-track .eos-nau-thumb{left:25px;background:#00e58a}.eos-nau-switch strong{align-self:end;font-size:.82rem}.eos-nau-switch small{align-self:start;color:#83a2b2;font-size:.67rem}.eos-nau-switch input:focus-visible+.eos-nau-track{outline:2px solid #4bdcff;outline-offset:3px}@media(max-width:760px){.eos-nexowatt-auto-update{align-items:flex-start;flex-direction:column}.eos-nau-switch{align-self:flex-end}}
@@ -17,7 +17,7 @@
17
17
  }
18
18
  #eos-ems-overview-runtime { width: calc(100% - 32px); max-width: none; margin: 0 16px 20px; }
19
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; }
20
+ .eos-ems-overview-header { display:flex; align-items:flex-start; justify-content:flex-start; gap:20px; }
21
21
  .eos-ems-overview-header h2 { margin:5px 0 6px; font-size:clamp(1.2rem,2vw,1.65rem); color:#f5fbff; }
22
22
  .eos-ems-overview-header p { margin:0; max-width:760px; color:#9eb2c2; line-height:1.45; }
23
23
  .eos-ems-overview-eyebrow { color:#49e39e; font-size:.72rem; font-weight:900; letter-spacing:.13em; text-transform:uppercase; }
@@ -48,9 +48,7 @@
48
48
  .eos-ems-overview-event time { color:#6f8799; font-size:.68rem; font-variant-numeric:tabular-nums; }
49
49
  .eos-ems-overview-event div { display:grid; gap:2px; min-width:0; }
50
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; }
51
+ .eos-ems-overview-footer { display:flex; justify-content:flex-start; gap:12px; margin-top:12px; color:#60798a; font-size:.67rem; }
54
52
  @media (max-width: 1100px) { .eos-ems-overview-card { flex-basis:100%; max-width:none; } }
55
53
  @media (max-width: 760px) {
56
54
  #eos-ems-overview-runtime,.eos-ems-overview-card { min-width:0; width:calc(100% - 16px); margin:8px; padding:16px; }
@@ -61,7 +59,7 @@
61
59
  .eos-ems-overview-footer { display:grid; }
62
60
  }
63
61
 
64
- /* v98: startup and compatibility messages stay compact. */
62
+ /* v100: startup and compatibility messages stay compact. */
65
63
  .eos-ems-overview-card.is-loading,
66
64
  .eos-ems-overview-card.is-unavailable {
67
65
  min-height: 0;
@@ -1292,7 +1292,7 @@ html.eos-route-intro #app-paper [class*="card"] {
1292
1292
  .eos-assist-header-root .eos-assist-panel { right: 8px !important; width: calc(100vw - 16px); }
1293
1293
  }
1294
1294
 
1295
- /* NexoWatt EOS 7.9.97 – orderly login, moderately enlarged. */
1295
+ /* NexoWatt EOS 7.9.100 – orderly login, moderately enlarged. */
1296
1296
  html.eos-login,
1297
1297
  html.eos-login body {
1298
1298
  min-height: 100%;
@@ -1407,119 +1407,33 @@ html.eos-login form { max-width: 100% !important; }
1407
1407
  html.eos-safe-overview-active #app-paper > :not(.MuiSnackbar-root):not([role="presentation"]) { pointer-events: auto !important; }
1408
1408
  .eos-intro-admin-edit-hidden { display: none !important; }
1409
1409
 
1410
- /* NexoWatt EOS 7.9.98 – stable scrolling, clean login fields and expandable errors. */
1411
- html.eos-app,
1412
- html.eos-app body,
1413
- html.eos-app #root {
1414
- height: 100% !important;
1415
- max-height: 100% !important;
1416
- overflow: hidden !important;
1417
- }
1418
- html.eos-app #app-paper {
1419
- height: calc(100vh - var(--nx-content-top) - 12px) !important;
1420
- height: calc(100dvh - var(--nx-content-top) - 12px) !important;
1421
- min-height: 0 !important;
1422
- max-height: calc(100vh - var(--nx-content-top) - 12px) !important;
1423
- max-height: calc(100dvh - var(--nx-content-top) - 12px) !important;
1424
- overflow-x: hidden !important;
1425
- overflow-y: auto !important;
1426
- overscroll-behavior: contain;
1427
- scrollbar-gutter: stable;
1428
- scrollbar-width: thin;
1429
- scrollbar-color: rgba(0, 229, 138, .88) rgba(3, 18, 29, .72);
1430
- }
1431
- html.eos-app #app-paper::-webkit-scrollbar { width: 11px; }
1432
- html.eos-app #app-paper::-webkit-scrollbar-track {
1433
- border-radius: 12px;
1434
- background: rgba(3, 18, 29, .72);
1435
- }
1436
- html.eos-app #app-paper::-webkit-scrollbar-thumb {
1437
- min-height: 46px;
1438
- border: 2px solid rgba(3, 18, 29, .72);
1439
- border-radius: 12px;
1440
- background: linear-gradient(180deg, #00e58a, #39c9ef);
1441
- }
1442
- html.eos-app #app-paper::-webkit-scrollbar-thumb:hover {
1443
- background: linear-gradient(180deg, #21f2a1, #60dcff);
1444
- }
1445
1410
 
1446
- html.eos-login main.MuiPaper-root::before {
1447
- width: min(456px, calc(100vw - 20px)) !important;
1448
- height: min(536px, calc(100dvh - 20px)) !important;
1449
- border-radius: 24px !important;
1450
- }
1451
- html.eos-login main.MuiPaper-root > .eos-login-card-modern,
1452
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),
1453
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password) {
1454
- width: min(400px, calc(100vw - 48px)) !important;
1455
- max-width: 400px !important;
1456
- min-height: 0 !important;
1457
- height: auto !important;
1458
- padding: 30px 32px 28px !important;
1459
- gap: 0 !important;
1460
- overflow: visible !important;
1461
- border-radius: 22px !important;
1462
- }
1463
- html.eos-login .eos-login-card-modern > .MuiGrid2-root,
1464
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username) > .MuiGrid2-root,
1465
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password) > .MuiGrid2-root {
1466
- width: 100% !important;
1467
- min-width: 0 !important;
1468
- }
1469
- html.eos-login .MuiFormControl-root {
1411
+ /* NexoWatt EOS 7.9.100 – edit controls belong to the lowest Intro card grid. */
1412
+ html.eos-route-intro .eos-native-intro-grid {
1413
+ position: relative !important;
1470
1414
  width: 100% !important;
1471
- margin: 13px 0 3px !important;
1472
- }
1473
- html.eos-login .MuiInputLabel-root { display: none !important; }
1474
- html.eos-login .MuiOutlinedInput-root {
1475
- min-height: 46px !important;
1476
- border-radius: 6px !important;
1477
- background: rgba(4, 19, 31, .76) !important;
1478
- }
1479
- html.eos-login .MuiOutlinedInput-input {
1480
- min-height: 0 !important;
1481
- padding: 11px 14px !important;
1482
- line-height: 22px !important;
1483
- }
1484
- html.eos-login .MuiOutlinedInput-input::placeholder {
1485
- color: rgba(245, 251, 255, .92) !important;
1486
- opacity: 1 !important;
1487
- }
1488
- html.eos-login .MuiOutlinedInput-notchedOutline legend {
1489
- width: 0 !important;
1490
- max-width: 0 !important;
1491
- padding: 0 !important;
1492
- }
1493
- html.eos-login .MuiFormControlLabel-root {
1494
- margin: 7px 0 5px !important;
1495
- }
1496
- html.eos-login .MuiButton-root[type="submit"] {
1497
- min-height: 44px !important;
1498
- margin: 8px 0 0 !important;
1499
- }
1500
- html.eos-login .eos-login-card-modern > .MuiBox-root:last-child,
1501
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username) > .MuiBox-root:last-child,
1502
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password) > .MuiBox-root:last-child {
1503
- margin-top: 28px !important;
1415
+ min-height: 80px !important;
1416
+ padding-bottom: 80px !important;
1417
+ overflow: visible !important;
1504
1418
  }
1505
- html.eos-login .MuiAlert-root,
1506
- html.eos-login [role="alert"] {
1507
- margin: 14px 0 1px !important;
1508
- padding: 9px 11px !important;
1509
- line-height: 1.35 !important;
1419
+ html.eos-route-intro .eos-overview-edit-controls {
1420
+ position: absolute !important;
1421
+ right: 16px !important;
1422
+ bottom: 16px !important;
1423
+ z-index: 3 !important;
1424
+ display: flex !important;
1425
+ align-items: center !important;
1426
+ gap: 8px !important;
1427
+ min-width: 52px !important;
1428
+ min-height: 52px !important;
1429
+ padding: 6px !important;
1430
+ border: 1px solid rgba(0, 229, 138, .24) !important;
1431
+ border-radius: 16px !important;
1432
+ background: rgba(3, 23, 34, .94) !important;
1433
+ box-shadow: 0 12px 30px rgba(0, 0, 0, .26) !important;
1510
1434
  }
1511
- @media (max-width: 500px) {
1512
- html.eos-login main.MuiPaper-root::before {
1513
- width: calc(100vw - 12px) !important;
1514
- height: auto !important;
1515
- min-height: min(520px, calc(100dvh - 12px)) !important;
1516
- }
1517
- html.eos-login main.MuiPaper-root > .eos-login-card-modern,
1518
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),
1519
- html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password) {
1520
- width: calc(100vw - 38px) !important;
1521
- max-width: none !important;
1522
- padding: 25px 22px 23px !important;
1523
- }
1435
+ html.eos-route-intro .eos-overview-edit-controls .MuiFab-root {
1436
+ position: static !important;
1437
+ inset: auto !important;
1438
+ margin: 0 !important;
1524
1439
  }
1525
-
@@ -8,8 +8,9 @@
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=98"></script>
12
- <script type="module" src="./assets/hostInit-v84.js?v=84"></script>
11
+ <script src="./js/eos-release-watch.js?v=100"></script>
12
+ <script src="./js/eos-manual-write-policy.js?v=100"></script>
13
+ <script type="module" src="./assets/hostInit-v84.js?v=100"></script>
13
14
 
14
15
  <meta charset="utf-8" />
15
16
  <link
@@ -36,8 +37,9 @@
36
37
  rel="stylesheet"
37
38
  href="css/leaflet.css"
38
39
  />
39
- <link rel="stylesheet" href="./css/nexowatt-native-shell.css?v=98" />
40
- <link rel="stylesheet" href="./css/eos-ems-overview.css?v=98" />
40
+ <link rel="stylesheet" href="./css/nexowatt-native-shell.css?v=100" />
41
+ <link rel="stylesheet" href="./css/eos-ems-overview.css?v=100" />
42
+ <link rel="stylesheet" href="./css/eos-auto-update.css?v=100" />
41
43
  <link
42
44
  rel="manifest"
43
45
  href="manifest.json"
@@ -157,23 +159,24 @@
157
159
  false,
158
160
  );
159
161
  </script>
160
- <link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js">
161
- <link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js">
162
- <script defer src="./js/eos-role-bootstrap.js?v=98" data-eos-entry="./assets/index-CQZugZ1z-v84.js?v=84"></script>
163
- <script defer src="./js/eos-policy-client.js?v=98"></script>
164
- <script defer src="./js/eos-branding-sanitizer.js?v=98"></script>
165
- <script defer src="./js/nexowatt-native-shell.js?v=98"></script>
166
- <script defer src="./js/eos-native-security.js?v=98"></script>
167
- <script defer src="./js/eos-basic-settings.js?v=98"></script>
168
- <script defer src="./js/eos-role-ui.js?v=98"></script>
169
- <script defer src="./js/eos-ems-overview.js?v=98"></script>
170
- <script defer src="./js/eos-account-management.js?v=98"></script>
171
- <script defer src="./js/eos-assistant.js?v=98"></script>
162
+ <link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js?v=100">
163
+ <link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js?v=100">
164
+ <script defer src="./js/eos-role-bootstrap.js?v=100" data-eos-entry="./assets/index-CQZugZ1z-v84.js?v=100"></script>
165
+ <script defer src="./js/eos-policy-client.js?v=100"></script>
166
+ <script defer src="./js/eos-branding-sanitizer.js?v=100"></script>
167
+ <script defer src="./js/nexowatt-native-shell.js?v=100"></script>
168
+ <script defer src="./js/eos-native-security.js?v=100"></script>
169
+ <script defer src="./js/eos-basic-settings.js?v=100"></script>
170
+ <script defer src="./js/eos-role-ui.js?v=100"></script>
171
+ <script defer src="./js/eos-ems-overview.js?v=100"></script>
172
+ <script defer src="./js/eos-account-management.js?v=100"></script>
173
+ <script defer src="./js/eos-auto-update.js?v=100"></script>
174
+ <script defer src="./js/eos-assistant.js?v=100"></script>
172
175
  </head>
173
176
  <body>
174
177
  <noscript>Für NexoWatt EOS muss JavaScript aktiviert sein.</noscript>
175
178
  <div id="root"></div>
176
- <script defer src="static/js/nexowatt-stable-login.js?v=98"></script>
177
- <script defer src="static/js/nexowatt-stable-v98.js?v=98"></script>
179
+ <script defer src="static/js/nexowatt-stable-login.js?v=100"></script>
180
+ <script defer src="static/js/nexowatt-stable-v100.js?v=100"></script>
178
181
  </body>
179
182
  </html>
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v98-account-management-real-password-write';
4
+ const VERSION = 'v100-account-management-password-write';
5
5
  const script = document.currentScript || document.querySelector('script[src*="eos-account-management.js"]');
6
6
  const base = new URL('../', script?.src || window.location.href);
7
7
  const abort = new AbortController();
@@ -19,10 +19,11 @@
19
19
  introInstaller: 'Du kannst Endkundenzugänge zurücksetzen. Admin- und Installateurzugänge bleiben NexoWatt Service vorbehalten.',
20
20
  loading: 'Zugänge werden geladen …', empty: 'Keine passenden Konten gefunden.', close: 'Schließen', refresh: 'Aktualisieren',
21
21
  installer: 'Installateur', enduser: 'Endkunde / Gast', active: 'Passwort aktiv', first: 'Erstanmeldung offen', disabled: 'Deaktiviert',
22
- reset: 'Passwort zurücksetzen', resetting: 'Wird zurückgesetzt …', resetConfirm: name => `Soll der Zugang „${name}“ wirklich zurückgesetzt werden? Das bisherige Passwort wird ungültig. Danach gilt einmalig das Startpasswort „nexowatt“ und beim nächsten Login muss sofort ein persönliches Passwort vergeben werden.`,
23
- resetDone: name => `Der Zugang „${name}“ ist zurückgesetzt. Einmalig mit dem Startpasswort „nexowatt“ anmelden und anschließend direkt ein persönliches Passwort festlegen.`,
24
- resetFailed: 'Der Zugang konnte nicht zurückgesetzt werden.', permissionError: 'Für dieses Konto ist das Zurücksetzen nicht erlaubt.', originError: 'Die Sicherheitsprüfung der Anfrage ist fehlgeschlagen. Seite mit Strg+F5 neu laden.', disabledError: 'Das Konto ist deaktiviert und kann nicht zurückgesetzt werden.', notFoundError: 'Das Konto wurde nicht gefunden.', writeError: 'Das neue Startpasswort konnte nicht in das Benutzerkonto geschrieben werden.', lastSet: 'Letzte Passwortvergabe', lastReset: 'Letzter Reset', by: 'durch',
22
+ reset: 'Passwort zurücksetzen', resetting: 'Wird zurückgesetzt …', resetConfirm: name => `Soll der Zugang „${name}“ wirklich zurückgesetzt werden? Danach gilt einmalig das Startpasswort „nexowatt“ und anschließend muss ein persönliches Passwort vergeben werden.`,
23
+ resetDone: name => `Der Zugang „${name}“ wurde im EOS-Benutzerkonto auf „nexowatt“ zurückgesetzt. Nach der nächsten Anmeldung muss sofort ein persönliches Passwort vergeben werden.`,
24
+ resetFailed: 'Der Zugang konnte nicht zurückgesetzt werden.', lastSet: 'Letzte Passwortvergabe', lastReset: 'Letzter Reset', by: 'durch',
25
25
  security: 'Admin/Service darf Installateur und Endkunde zurücksetzen. Installateure dürfen ausschließlich Endkunden zurücksetzen.',
26
+ errors: { permissionError: 'Die Admin-/Installateurberechtigung konnte nicht bestätigt werden. Bitte neu anmelden.', invalidRequest: 'Die Sicherheitsbestätigung fehlt. Bitte die Seite neu laden.', invalidRequestOrigin: 'Die Sitzung oder Adresse hat sich geändert. Bitte die Seite neu laden.', invalidTarget: 'Dieses Konto darf nicht über EOS zurückgesetzt werden.', accountDisabled: 'Das Konto ist deaktiviert.', accountNotFound: 'Das Benutzerkonto wurde nicht gefunden.', passwordApiUnavailable: 'Die EOS-Passwortverwaltung ist nicht verfügbar.', passwordNotPersisted: 'Das Startpasswort wurde nicht gespeichert.', passwordVerificationFailed: 'Das Startpasswort wurde geschrieben, aber nicht als gültige Anmeldung bestätigt.', passwordMetadataNotPersisted: 'Der Erstanmeldungsstatus wurde nicht gespeichert.', accountResetFailed: 'Das Konto konnte serverseitig nicht zurückgesetzt werden.' },
26
27
  },
27
28
  en: {
28
29
  launcher: 'Accounts', titleAdmin: 'Accounts & roles', titleInstaller: 'End-user accounts',
@@ -30,10 +31,11 @@
30
31
  introInstaller: 'You may reset end-user accounts. Admin and installer accounts remain reserved for NexoWatt Service.',
31
32
  loading: 'Loading accounts …', empty: 'No matching accounts found.', close: 'Close', refresh: 'Refresh',
32
33
  installer: 'Installer', enduser: 'End user / Guest', active: 'Password active', first: 'First activation open', disabled: 'Disabled',
33
- reset: 'Reset password', resetting: 'Resetting …', resetConfirm: name => `Reset account “${name}”? The current password becomes invalid. Sign in once with the initial password “nexowatt”, then create a personal password immediately.`,
34
- resetDone: name => `Account “${name}” was reset. Sign in once with “nexowatt” and then create a personal password.`,
35
- resetFailed: 'The account could not be reset.', permissionError: 'Reset is not permitted for this account.', originError: 'The request security check failed. Reload the page with Ctrl+F5.', disabledError: 'The account is disabled and cannot be reset.', notFoundError: 'The account was not found.', writeError: 'The initial password could not be written to the user account.', lastSet: 'Last password setup', lastReset: 'Last reset', by: 'by',
34
+ reset: 'Reset password', resetting: 'Resetting …', resetConfirm: name => `Reset account “${name}”? The one-time initial password will be “nexowatt”; a personal password must then be created immediately.`,
35
+ resetDone: name => `Account “${name}” was reset in the EOS user account to “nexowatt”. The next sign-in requires an immediate personal password change.`,
36
+ resetFailed: 'The account could not be reset.', lastSet: 'Last password setup', lastReset: 'Last reset', by: 'by',
36
37
  security: 'Admin/Service may reset installer and end-user accounts. Installers may reset end-user accounts only.',
38
+ errors: { permissionError: 'Admin/Installer permission could not be confirmed. Sign in again.', invalidRequest: 'The security confirmation is missing. Reload the page.', invalidRequestOrigin: 'The session or address changed. Reload the page.', invalidTarget: 'This account cannot be reset through EOS.', accountDisabled: 'The account is disabled.', accountNotFound: 'The user account was not found.', passwordApiUnavailable: 'The EOS password service is unavailable.', passwordNotPersisted: 'The initial password was not saved.', passwordVerificationFailed: 'The initial password was written but could not be verified.', passwordMetadataNotPersisted: 'The first-login status was not saved.', accountResetFailed: 'The account could not be reset on the server.' },
37
39
  },
38
40
  nl: {
39
41
  launcher: 'Toegangen', titleAdmin: 'Toegangen & rollen', titleInstaller: 'Eindgebruikerstoegangen',
@@ -41,26 +43,15 @@
41
43
  introInstaller: 'Je kunt eindgebruikersaccounts resetten. Admin- en installateurstoegang blijft voor NexoWatt Service.',
42
44
  loading: 'Toegangen worden geladen …', empty: 'Geen passende accounts gevonden.', close: 'Sluiten', refresh: 'Vernieuwen',
43
45
  installer: 'Installateur', enduser: 'Eindgebruiker / Guest', active: 'Wachtwoord actief', first: 'Eerste activering open', disabled: 'Uitgeschakeld',
44
- reset: 'Wachtwoord resetten', resetting: 'Wordt gereset …', resetConfirm: name => `Account „${name}“ resetten? Het huidige wachtwoord wordt ongeldig. Meld één keer aan met het startwachtwoord „nexowatt“ en stel daarna direct een persoonlijk wachtwoord in.`,
45
- resetDone: name => `Account „${name}“ is gereset. Meld één keer aan met „nexowatt“ en stel daarna een persoonlijk wachtwoord in.`,
46
- resetFailed: 'Het account kon niet worden gereset.', permissionError: 'Resetten is voor dit account niet toegestaan.', originError: 'De veiligheidscontrole van het verzoek is mislukt. Herlaad de pagina met Ctrl+F5.', disabledError: 'Het account is uitgeschakeld en kan niet worden gereset.', notFoundError: 'Het account werd niet gevonden.', writeError: 'Het startwachtwoord kon niet naar het gebruikersaccount worden geschreven.', lastSet: 'Laatste wachtwoordinstelling', lastReset: 'Laatste reset', by: 'door',
46
+ reset: 'Wachtwoord resetten', resetting: 'Wordt gereset …', resetConfirm: name => `Account „${name}“ resetten? Eenmalig geldt het startwachtwoord „nexowatt“, waarna direct een persoonlijk wachtwoord moet worden ingesteld.`,
47
+ resetDone: name => `Account „${name}“ is in het EOS-gebruikersaccount teruggezet op „nexowatt“. Bij de volgende aanmelding moet direct een persoonlijk wachtwoord worden ingesteld.`,
48
+ resetFailed: 'Het account kon niet worden gereset.', lastSet: 'Laatste wachtwoordinstelling', lastReset: 'Laatste reset', by: 'door',
47
49
  security: 'Admin/service mag installateur- en eindgebruikersaccounts resetten. Installateurs alleen eindgebruikers.',
50
+ errors: { permissionError: 'De Admin-/Installateurrechten konden niet worden bevestigd. Meld opnieuw aan.', invalidRequest: 'De beveiligingsbevestiging ontbreekt. Herlaad de pagina.', invalidRequestOrigin: 'De sessie of het adres is gewijzigd. Herlaad de pagina.', invalidTarget: 'Dit account kan niet via EOS worden gereset.', accountDisabled: 'Het account is uitgeschakeld.', accountNotFound: 'Het gebruikersaccount is niet gevonden.', passwordApiUnavailable: 'De EOS-wachtwoordservice is niet beschikbaar.', passwordNotPersisted: 'Het startwachtwoord is niet opgeslagen.', passwordVerificationFailed: 'Het startwachtwoord is geschreven maar kon niet worden geverifieerd.', passwordMetadataNotPersisted: 'De eerste-aanmeldstatus is niet opgeslagen.', accountResetFailed: 'Het account kon server-side niet worden gereset.' },
48
51
  },
49
52
  };
50
53
  const t = () => i18n[language()];
51
- const accountErrorText = code => {
52
- const text = t();
53
- return ({
54
- permissionError: text.permissionError,
55
- invalidRequestOrigin: text.originError,
56
- accountDisabled: text.disabledError,
57
- accountNotFound: text.notFoundError,
58
- passwordApiUnavailable: text.writeError,
59
- passwordWriteFailed: text.writeError,
60
- passwordVerificationFailed: text.writeError,
61
- userObjectUnavailableAfterPasswordChange: text.writeError,
62
- })[String(code || '')] || text.resetFailed;
63
- };
54
+ const errorText = code => t().errors?.[String(code || '')] || String(code || t().resetFailed);
64
55
  const roleFromPolicy = policy => {
65
56
  const raw = normalize(policy?.role || '');
66
57
  if (policy?.isAdmin || policy?.isEosAdminGroup || /admin|service/.test(raw)) return 'admin';
@@ -157,17 +148,17 @@
157
148
  reset.disabled = true; reset.textContent = t().resetting; setStatus('', '');
158
149
  try {
159
150
  const response = await fetch(new URL('nexowatt/account/reset', base).href, {
160
- method: 'POST', credentials: 'include', cache: 'no-store',
151
+ method: 'POST', credentials: 'same-origin', cache: 'no-store',
161
152
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-NexoWatt-EOS-Account-Reset': '1' },
162
153
  body: JSON.stringify({ user: account.id }),
163
154
  });
164
155
  const payload = await response.json().catch(() => ({}));
165
- if (!response.ok || payload.error) throw new Error(accountErrorText(payload.error));
156
+ if (!response.ok || payload.error) throw new Error(payload.error || t().resetFailed);
166
157
  setStatus(t().resetDone(label), 'success');
167
158
  await loadAccounts();
168
159
  } catch (error) {
169
160
  reset.disabled = false; reset.textContent = t().reset;
170
- setStatus(error?.message || t().resetFailed, 'error');
161
+ setStatus(errorText(error?.message), 'error');
171
162
  }
172
163
  });
173
164
  actions.appendChild(reset);
@@ -189,12 +180,12 @@
189
180
  if (!state.overlay) return;
190
181
  setStatus(t().loading, '');
191
182
  try {
192
- const response = await fetch(new URL('nexowatt/account/manage', base).href, { credentials: 'include', cache: 'no-store', headers: { Accept: 'application/json' } });
183
+ const response = await fetch(new URL('nexowatt/account/manage', base).href, { credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' } });
193
184
  const data = await response.json().catch(() => ({}));
194
185
  if (!response.ok || data.error) throw new Error(data.error || t().resetFailed);
195
186
  renderAccounts(data); setStatus('', '');
196
187
  } catch (error) {
197
- setStatus(error?.message || t().resetFailed, 'error');
188
+ setStatus(errorText(error?.message), 'error');
198
189
  }
199
190
  };
200
191
 
@@ -1,6 +1,6 @@
1
1
  (() => {
2
2
  'use strict';
3
- // NexoWatt EOS stable 7.9.97: EOS Assist remains disabled until the
3
+ // NexoWatt EOS stable 7.9.100: 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,78 @@
1
+ (() => {
2
+ 'use strict';
3
+ const VERSION = 'v100-nexowatt-stable-auto-update';
4
+ const ROOT_ID = 'eos-nexowatt-auto-update';
5
+ const script = document.currentScript;
6
+ const API = new URL('../nexowatt/updates/', script?.src || document.baseURI).href;
7
+ let busy = false;
8
+ let lastLoad = 0;
9
+ let pollTimer = 0;
10
+
11
+ const role = () => String(window.NEXOWATT_EOS_ACCESS_ROLE || 'admin').toLowerCase();
12
+ const introActive = () => String(location.hash || '').replace(/^#\/?/, '').split(/[/?&]/)[0] === 'tab-intro';
13
+ const esc = value => String(value ?? '').replace(/[&<>'"]/g, char => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[char]));
14
+ const formatTime = value => value ? new Intl.DateTimeFormat(undefined, { dateStyle: 'short', timeStyle: 'medium' }).format(new Date(value)) : '–';
15
+
16
+ const ensureRoot = () => {
17
+ if (role() !== 'admin' || !introActive()) { document.getElementById(ROOT_ID)?.remove(); return null; }
18
+ let root = document.getElementById(ROOT_ID);
19
+ if (root) return root;
20
+ root = document.createElement('section');
21
+ root.id = ROOT_ID;
22
+ root.className = 'eos-nexowatt-auto-update';
23
+ const hero = document.getElementById('eos-native-overview-hero');
24
+ const ems = document.getElementById('eos-ems-overview-runtime');
25
+ const parent = ems?.parentElement || hero?.parentElement || document.getElementById('app-paper');
26
+ if (!parent) return null;
27
+ if (ems) parent.insertBefore(root, ems); else if (hero?.nextSibling) parent.insertBefore(root, hero.nextSibling); else parent.append(root);
28
+ return root;
29
+ };
30
+
31
+ const render = status => {
32
+ const root = ensureRoot(); if (!root) return;
33
+ const enabled = status?.enabled !== false;
34
+ const managed = Array.isArray(status?.managedAdapters) ? status.managedAdapters : [];
35
+ const error = String(status?.error || '');
36
+ root.dataset.enabled = String(enabled);
37
+ root.innerHTML = `<div class="eos-nau-copy"><span class="eos-nau-eyebrow">NexoWatt Stable</span><h2>Automatische Adapter-Updates</h2><p>${enabled ? 'Installierte NexoWatt-Adapter werden automatisch aktualisiert, sobald im erkannten Stable-Repository eine neuere stabile Version verfügbar ist.' : 'Automatische NexoWatt-Updates sind deaktiviert. Vorherige ioBroker-Update-Richtlinien wurden wiederhergestellt.'}</p><div class="eos-nau-meta"><span>Repository: <strong>${esc(status?.repository || 'wird erkannt')}</strong></span><span>Letzter Abgleich: <strong>${esc(formatTime(status?.lastSync))}</strong></span><span>Richtlinie: <strong>alle neueren Stable-Versionen</strong></span></div>${managed.length ? `<div class="eos-nau-adapters">${managed.map(name => `<span>${esc(name)}</span>`).join('')}</div>` : ''}${error ? `<div class="eos-nau-error">${esc(error)}</div>` : ''}</div><label class="eos-nau-switch"><input type="checkbox" ${enabled ? 'checked' : ''} ${busy ? 'disabled' : ''}><span class="eos-nau-track"><span class="eos-nau-thumb"></span></span><strong>${enabled ? 'Aktiv' : 'Deaktiviert'}</strong><small>jederzeit änderbar</small></label>`;
38
+ root.querySelector('input')?.addEventListener('change', event => void save(Boolean(event.currentTarget.checked)));
39
+ };
40
+
41
+ const request = async (path, options = {}) => {
42
+ const response = await fetch(new URL(path, API), { cache: 'no-store', credentials: 'same-origin', ...options });
43
+ const data = await response.json().catch(() => ({}));
44
+ if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
45
+ return data;
46
+ };
47
+ const load = async (force = false) => {
48
+ if (role() !== 'admin' || !introActive()) return;
49
+ if (!force && Date.now() - lastLoad < 5000) return;
50
+ lastLoad = Date.now();
51
+ try { render(await request('status')); }
52
+ catch (error) { render({ enabled: true, error: error?.message || error }); }
53
+ };
54
+ const save = async enabled => {
55
+ if (busy) return;
56
+ busy = true;
57
+ try {
58
+ render({ enabled });
59
+ const status = await request('settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-NexoWatt-EOS-Auto-Update': '1' }, body: JSON.stringify({ enabled }) });
60
+ render(status);
61
+ } catch (error) {
62
+ render({ enabled: !enabled, error: error?.message || error });
63
+ } finally { busy = false; }
64
+ };
65
+
66
+ let scheduled = false;
67
+ const schedule = () => { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { scheduled = false; void load(); }); };
68
+ const start = () => {
69
+ void load();
70
+ window.addEventListener('hashchange', () => { lastLoad = 0; schedule(); });
71
+ window.addEventListener('nexowatt-eos-admin-socket-ready', schedule);
72
+ document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') schedule(); });
73
+ new MutationObserver(() => { if (!document.getElementById(ROOT_ID) && introActive()) schedule(); }).observe(document.documentElement, { childList: true, subtree: true });
74
+ pollTimer = window.setInterval(() => void load(true), 30000);
75
+ };
76
+ document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, { once: true }) : start();
77
+ window.NEXOWATT_EOS_AUTO_UPDATE_UI = Object.freeze({ version: VERSION, refresh: load });
78
+ })();
@@ -1,6 +1,6 @@
1
1
  (() => {
2
2
  'use strict';
3
- const VERSION = 'v98-nexowatt-ems-overview-runtime';
3
+ const VERSION = 'v100-nexowatt-ems-overview-runtime';
4
4
  const CONTRACT = 'nexowatt-ems-overview-v1';
5
5
  const ROOT_ID = 'eos-ems-overview-runtime';
6
6
  const POLL_MS = 5000;
@@ -161,11 +161,10 @@
161
161
  if (tariff.available) tags.push({ label: `Tarif: ${text(tariff.state, 'aktiv')}${tariff.priceEurPerKwh != null ? ` · ${num(tariff.priceEurPerKwh).toFixed(3)} €/kWh` : ''}` });
162
162
  if (forecast.available) tags.push({ label: `Prognose: ${text(forecast.source, 'aktiv')}${forecast.fresh === false ? ' · veraltet' : ''}`, warning: forecast.fresh === false });
163
163
  if (ems.safetyActive || ems.safetyEmergencyStop || ems.safetyValid === false) tags.push({ label: 'EOS Safety aktiv', warning: true });
164
- 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}`}`;
165
164
  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>`;
166
165
  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>';
167
166
  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>`;
168
- 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>`;
167
+ 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></footer>`;
169
168
  };
170
169
 
171
170
  const scheduleFastRetry = () => {
@@ -0,0 +1,77 @@
1
+ (() => {
2
+ 'use strict';
3
+ const RELEASE = '7.9.100';
4
+ const RELEASE_KEY = 'nexowatt.eos.loadedRelease';
5
+ const RELOAD_KEY = 'nexowatt.eos.reloadTarget';
6
+ const CHECK_MS = 15000;
7
+ let refreshing = false;
8
+
9
+ const normalize = value => String(value || '').trim().replace(/^v/i, '');
10
+ const validRelease = value => /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(normalize(value));
11
+ const reloadUrl = version => {
12
+ const url = new URL(window.location.href);
13
+ url.searchParams.set('_eosRelease', normalize(version) || RELEASE);
14
+ url.searchParams.set('_eosReload', String(Date.now()));
15
+ return url.toString();
16
+ };
17
+ const purgeBrowserCaches = async () => {
18
+ const jobs = [];
19
+ try {
20
+ if ('caches' in window) {
21
+ jobs.push(caches.keys().then(keys => Promise.all(keys.map(key => caches.delete(key)))));
22
+ }
23
+ } catch (_) { /* Cache API is optional */ }
24
+ try {
25
+ if (navigator.serviceWorker?.getRegistrations) {
26
+ jobs.push(navigator.serviceWorker.getRegistrations().then(rows => Promise.all(rows.map(row => row.unregister()))));
27
+ }
28
+ } catch (_) { /* service workers may be unavailable */ }
29
+ await Promise.allSettled(jobs);
30
+ };
31
+ const reloadFor = async version => {
32
+ const target = normalize(version) || RELEASE;
33
+ if (refreshing || sessionStorage.getItem(RELOAD_KEY) === target) return;
34
+ refreshing = true;
35
+ sessionStorage.setItem(RELOAD_KEY, target);
36
+ document.documentElement.classList.add('eos-release-refreshing');
37
+ await purgeBrowserCaches();
38
+ window.location.replace(reloadUrl(target));
39
+ };
40
+ const checkServerVersion = async () => {
41
+ try {
42
+ const endpoint = new URL('./version', window.location.href);
43
+ endpoint.searchParams.set('_eosTs', String(Date.now()));
44
+ const response = await fetch(endpoint, {
45
+ cache: 'no-store',
46
+ credentials: 'same-origin',
47
+ headers: { 'Cache-Control': 'no-cache' },
48
+ });
49
+ if (!response.ok) return;
50
+ const serverVersion = normalize(await response.text());
51
+ if (validRelease(serverVersion) && serverVersion !== RELEASE) {
52
+ await reloadFor(serverVersion);
53
+ }
54
+ } catch (_) { /* adapter restart or temporary network interruption */ }
55
+ };
56
+
57
+ window.NEXOWATT_EOS_RELEASE = RELEASE;
58
+ const previous = normalize(localStorage.getItem(RELEASE_KEY));
59
+ localStorage.setItem(RELEASE_KEY, RELEASE);
60
+ const loadedFor = normalize(new URL(window.location.href).searchParams.get('_eosRelease'));
61
+ if (previous && previous !== RELEASE && loadedFor !== RELEASE) {
62
+ void reloadFor(RELEASE);
63
+ } else {
64
+ sessionStorage.removeItem(RELOAD_KEY);
65
+ }
66
+
67
+ window.addEventListener('focus', () => void checkServerVersion());
68
+ document.addEventListener('visibilitychange', () => {
69
+ if (document.visibilityState === 'visible') void checkServerVersion();
70
+ });
71
+ window.setInterval(() => void checkServerVersion(), CHECK_MS);
72
+ window.NEXOWATT_EOS_RELEASE_WATCH = Object.freeze({
73
+ release: RELEASE,
74
+ check: checkServerVersion,
75
+ purge: purgeBrowserCaches,
76
+ });
77
+ })();