iobroker.eos-admin 7.10.2 → 7.10.4

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.
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "eos-admin",
4
- "version": "7.10.2",
4
+ "version": "7.10.4",
5
5
  "titleLang": {
6
6
  "en": "NexoWatt EOS Admin",
7
7
  "de": "NexoWatt EOS Admin",
@@ -18,6 +18,11 @@
18
18
  "connectionType": "local",
19
19
  "dataSource": "push",
20
20
  "news": {
21
+ "7.10.4": {
22
+ "de": "Stable 7.10.4 verschiebt die Einstellung für automatische NexoWatt-Adapter-Updates vollständig in die Systemeinstellungen hinter dem Schraubenschlüssel. Auf Übersicht, Module und allen anderen Hauptseiten wird die Update-Karte nicht mehr eingeblendet. Die stabile Update-Logik und die Absturzkorrekturen aus 7.10.2 bleiben unverändert.",
23
+ "en": "Stable 7.10.4 moves the automatic NexoWatt adapter update control entirely into System Settings behind the wrench button. The update card is no longer shown on Overview, Modules or any other main page. Stable update behavior and the 7.10.2 crash fixes remain unchanged.",
24
+ "nl": "Stable 7.10.4 verplaatst de instelling voor automatische NexoWatt-adapterupdates volledig naar de systeeminstellingen achter de moersleutelknop. De updatekaart wordt niet meer weergegeven op Overzicht, Modules of andere hoofdpagina’s. De stabiele updatelogica en crashfixes uit 7.10.2 blijven ongewijzigd."
25
+ },
21
26
  "7.10.2": {
22
27
  "de": "Stable-Hotfix 7.10.2 beseitigt die EOS-Admin-Startschleife: Stable-Update-Status wird nicht mehr in die laufende Instanzkonfiguration geschrieben, der erste Abgleich startet verzögert und das Release prüft die vollständige Backend-Modulkette einschließlich eosRequestSecurity.",
23
28
  "en": "Stable hotfix 7.10.2 removes the EOS Admin restart loop: stable-update status is no longer written to the running instance configuration, first reconciliation is delayed, and the release validates the complete backend module chain including eosRequestSecurity.",
@@ -344,7 +349,7 @@
344
349
  "icon": "admin.svg",
345
350
  "messagebox": true,
346
351
  "enabled": true,
347
- "extIcon": "https://unpkg.com/iobroker.eos-admin@7.10.2/admin/admin.svg",
352
+ "extIcon": "https://unpkg.com/iobroker.eos-admin@7.10.4/admin/admin.svg",
348
353
  "keywords": [
349
354
  "NexoWatt",
350
355
  "EOS",
@@ -355,7 +360,7 @@
355
360
  "licensed"
356
361
  ],
357
362
  "compact": true,
358
- "readme": "https://unpkg.com/iobroker.eos-admin@7.10.2/README.md",
363
+ "readme": "https://unpkg.com/iobroker.eos-admin@7.10.4/README.md",
359
364
  "authors": [
360
365
  "NexoWatt"
361
366
  ],
@@ -423,7 +428,7 @@
423
428
  "nondeletable": false,
424
429
  "allowAdapterUpdate": true,
425
430
  "allowAdapterDelete": false,
426
- "meta": "https://unpkg.com/iobroker.eos-admin@7.10.2/io-package.json",
431
+ "meta": "https://unpkg.com/iobroker.eos-admin@7.10.4/io-package.json",
427
432
  "npmPackage": "iobroker.eos-admin",
428
433
  "packetName": "iobroker.eos-admin",
429
434
  "publisher": "NexoWatt"
@@ -1183,5 +1188,5 @@
1183
1188
  }
1184
1189
  }
1185
1190
  },
1186
- "version": "7.10.2"
1191
+ "version": "7.10.4"
1187
1192
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iobroker.eos-admin",
3
3
  "description": "NexoWatt EOS Admin – repaired stable baseline with restart-safe automatic updates, native account passwords and persistent cockpit scrolling.",
4
- "version": "7.10.2",
4
+ "version": "7.10.4",
5
5
  "contributors": [
6
6
  "bluefox <dogafox@gmail.com>",
7
7
  "apollon77",
@@ -132,13 +132,16 @@
132
132
  "tools/nexowatt-backend-runtime-selftest.cjs",
133
133
  "NEXOWATT_EOS_PREBUILT_MANIFEST.json",
134
134
  "tools/nexowatt-prebuilt-release-selftest.cjs",
135
- "tools/nexowatt-stable-v7102-selftest.cjs",
136
- "README_STABILITY_V7.10.2.md",
137
- "RELEASE_NOTES_V7.10.2.md",
138
- "PUBLISH_STABLE_V7.10.2.md",
139
- "INSTALL_TEST_V7.10.2.md",
140
- "RELEASE_ACCEPTANCE_V7.10.2.md",
141
- "MERGE_UPDATE_README_V7.10.2.md"
135
+ "tools/nexowatt-stable-v7103-selftest.cjs",
136
+ "README_STABILITY_V7.10.4.md",
137
+ "RELEASE_NOTES_V7.10.4.md",
138
+ "PUBLISH_STABLE_V7.10.4.md",
139
+ "INSTALL_TEST_V7.10.4.md",
140
+ "RELEASE_ACCEPTANCE_V7.10.4.md",
141
+ "MERGE_UPDATE_README_V7.10.4.md",
142
+ "tools/nexowatt-auto-update-placement-selftest.cjs",
143
+ "tools/nexowatt-role-security-selftest.cjs",
144
+ "src-admin/nexowatt-role-security.js"
142
145
  ],
143
146
  "scripts": {
144
147
  "npm": "npm i -f && cd src-admin && npm i -f",
@@ -173,7 +176,7 @@
173
176
  "nexowatt:repo-entry": "node tools/nexowatt-generate-repo-entry.cjs",
174
177
  "nexowatt:patch-repo": "node tools/nexowatt-patch-repo.cjs",
175
178
  "nexowatt:repair-update": "node tools/nexowatt-repair-eos-admin-update.cjs",
176
- "check:eos-stability": "node tools/nexowatt-stability-check.cjs && node tools/nexowatt-esm-syntax-selftest.cjs && node tools/nexowatt-backend-runtime-selftest.cjs && node tools/nexowatt-prebuilt-release-selftest.cjs && node tools/nexowatt-import-integrity-selftest.cjs && node tools/nexowatt-entrypoint-smoke-selftest.cjs && node tools/nexowatt-frontend-graph-check.cjs && node tools/nexowatt-policy-selftest.cjs && node tools/nexowatt-publish-channel-selftest.cjs && node tools/nexowatt-release-defaults-selftest.cjs && node tools/nexowatt-role-access-selftest.cjs && node tools/nexowatt-internal-reserve-selftest.cjs && node tools/nexowatt-modern-ui-selftest.cjs && node tools/nexowatt-login-layout-selftest.cjs && node tools/nexowatt-branding-selftest.cjs && node tools/nexowatt-default-port-selftest.cjs && node tools/nexowatt-module-popup-selftest.cjs && node tools/nexowatt-datapoint-selftest.cjs && node tools/nexowatt-update-selftest.cjs && node tools/nexowatt-datapoint-interaction-selftest.cjs && node tools/nexowatt-manual-write-policy-selftest.cjs && node tools/nexowatt-value-column-selftest.cjs && node tools/nexowatt-native-shell-selftest.cjs && node tools/nexowatt-assistant-separation-selftest.cjs && node tools/nexowatt-runtime-cleanup-selftest.cjs && node tools/nexowatt-version-sync-selftest.cjs && node tools/nexowatt-merge-update-selftest.cjs && node tools/nexowatt-password-write-selftest.cjs && node tools/nexowatt-account-password-flow-selftest.cjs && npm run check:eos-stable-v7102 && node tools/nexowatt-ems-overview-selftest.cjs && node tools/nexowatt-ui-overview-runtime-selftest.cjs && node tools/nexowatt-auto-update-selftest.cjs",
179
+ "check:eos-stability": "node tools/nexowatt-stability-check.cjs && node tools/nexowatt-esm-syntax-selftest.cjs && node tools/nexowatt-backend-runtime-selftest.cjs && node tools/nexowatt-prebuilt-release-selftest.cjs && node tools/nexowatt-import-integrity-selftest.cjs && node tools/nexowatt-entrypoint-smoke-selftest.cjs && node tools/nexowatt-frontend-graph-check.cjs && node tools/nexowatt-policy-selftest.cjs && node tools/nexowatt-publish-channel-selftest.cjs && node tools/nexowatt-release-defaults-selftest.cjs && node tools/nexowatt-role-access-selftest.cjs && node tools/nexowatt-internal-reserve-selftest.cjs && node tools/nexowatt-modern-ui-selftest.cjs && node tools/nexowatt-login-layout-selftest.cjs && node tools/nexowatt-branding-selftest.cjs && node tools/nexowatt-default-port-selftest.cjs && node tools/nexowatt-module-popup-selftest.cjs && node tools/nexowatt-datapoint-selftest.cjs && node tools/nexowatt-update-selftest.cjs && node tools/nexowatt-datapoint-interaction-selftest.cjs && node tools/nexowatt-manual-write-policy-selftest.cjs && node tools/nexowatt-value-column-selftest.cjs && node tools/nexowatt-native-shell-selftest.cjs && node tools/nexowatt-assistant-separation-selftest.cjs && node tools/nexowatt-runtime-cleanup-selftest.cjs && node tools/nexowatt-version-sync-selftest.cjs && node tools/nexowatt-merge-update-selftest.cjs && node tools/nexowatt-password-write-selftest.cjs && node tools/nexowatt-account-password-flow-selftest.cjs && npm run check:eos-stable-v7103 && node tools/nexowatt-ems-overview-selftest.cjs && node tools/nexowatt-ui-overview-runtime-selftest.cjs && node tools/nexowatt-auto-update-selftest.cjs && node tools/nexowatt-auto-update-placement-selftest.cjs && node tools/nexowatt-role-security-selftest.cjs",
177
180
  "nexowatt:patch-built-frontend": "node tools/nexowatt-patch-built-frontend.cjs",
178
181
  "clean:eos-runtime": "node tools/nexowatt-clean-legacy-runtime.cjs",
179
182
  "precheck:eos-package": "npm run sync:eos-version && npm run prepare:eos-release-defaults && npm run clean:eos-runtime",
@@ -198,9 +201,11 @@
198
201
  "test:eos-version-sync": "node tools/nexowatt-version-sync-selftest.cjs",
199
202
  "verify:eos-merge": "node tools/nexowatt-merge-update-selftest.cjs",
200
203
  "check:eos-backend-runtime": "node tools/nexowatt-backend-runtime-selftest.cjs",
201
- "check:eos-stable-v7102": "node tools/nexowatt-stable-v7102-selftest.cjs",
202
204
  "check:eos-prebuilt-release": "node tools/nexowatt-prebuilt-release-selftest.cjs",
203
- "seal:eos-prebuilt-release": "node tools/nexowatt-prebuilt-release-selftest.cjs --write"
205
+ "seal:eos-prebuilt-release": "node tools/nexowatt-prebuilt-release-selftest.cjs --write",
206
+ "check:eos-stable-v7103": "node tools/nexowatt-stable-v7103-selftest.cjs",
207
+ "test:eos-auto-update-placement": "node tools/nexowatt-auto-update-placement-selftest.cjs",
208
+ "check:eos-role-security": "node tools/nexowatt-role-security-selftest.cjs"
204
209
  },
205
210
  "author": "NexoWatt",
206
211
  "private": false,
@@ -211,7 +216,7 @@
211
216
  },
212
217
  "nexowattReleasePolicy": {
213
218
  "distTag": "latest",
214
- "purpose": "accepted stable NexoWatt EOS sales-product candidate 7.10.2"
219
+ "purpose": "accepted stable NexoWatt EOS sales-product candidate 7.10.4"
215
220
  },
216
221
  "publisher": "NexoWatt"
217
222
  }
@@ -0,0 +1,69 @@
1
+ # NexoWatt EOS Admin Frontend Source
2
+
3
+ Dieser Bereich enthält den Frontend-Quellcode der Administrationsoberfläche. Das sichtbare EOS-Branding wird über die fertigen Assets in `adminWww/` und die NexoWatt-spezifischen Branding-Dateien ergänzt.
4
+
5
+ Die folgenden Komponentenhinweise stammen aus der technischen Frontend-Basis und bleiben für Entwicklung und Wartung erhalten.
6
+
7
+ ---
8
+
9
+ # Components
10
+
11
+ ## DrawerItem
12
+
13
+ ![DrawerItem (light)](docs/images/components/DrawerItem_light.png)
14
+ ![DrawerItem (dark blue)](docs/images/components/DrawerItem_darkblue.png)
15
+ ![DrawerItem (dark)](docs/images/components/DrawerItem_dark.png)
16
+
17
+ ### Import
18
+
19
+ ```js
20
+ import DrawerItem from './components/DrawerItem';
21
+ ```
22
+
23
+ ### Props
24
+
25
+ | Name | Default | Description |
26
+ | ------------ | ------- | -------------------------------------- |
27
+ | icon | | The icon on the left side |
28
+ | onClick | | Function that is called when clicking |
29
+ | selected | | Indicates whether the item is selected |
30
+ | text | | Text to display |
31
+ | badgeContent | 0 | Content of the badge |
32
+ | badgeColor | primary | Color of the badge |
33
+
34
+ ### Example
35
+
36
+ ```js
37
+ <DrawerItem
38
+ text="Logs"
39
+ selected={false}
40
+ badgeContent={11}
41
+ badgeColor={'error'}
42
+ />
43
+ ```
44
+
45
+ ## IntroCard
46
+
47
+ ![IntroCard (light)](docs/images/components/IntroCard_light.png)
48
+ ![IntroCard (dark blue)](docs/images/components/IntroCard_darkblue.png)
49
+ ![IntroCard (dark)](docs/images/components/IntroCard_dark.png)
50
+
51
+ ### Import
52
+
53
+ ```js
54
+ import IntroCard from './components/IntroCard';
55
+ ```
56
+
57
+ ### Props
58
+
59
+ | Name | Default | Description |
60
+ |------------------| ------- |-------------------------------------------------------------------------------------|
61
+ | title | | The title of the card |
62
+ | children | | The child elements |
63
+ | image | | The image to display on the left |
64
+ | color | #e2e2e2 | The color to display as the background of the image |
65
+ | action | | Link and text {link: 'https://example.com', text: 'example.com'} |
66
+ | reveal | | Element or text that should be displayed in the reveal element using an info button |
67
+ | enabled | | State of the card |
68
+ | edit | | Editing mode active |
69
+ | toggleActivation | | Function for switching the status |
@@ -0,0 +1,336 @@
1
+ (() => {
2
+ "use strict";
3
+
4
+ const VERSION = "7.10.4";
5
+ const HIDDEN = "data-eos-role-hidden";
6
+ const READONLY = "data-eos-readonly";
7
+ const state = { role: "unknown", backupActive: null, user: "", appliedAt: 0 };
8
+
9
+ const adminOnlyTokens = [
10
+ "app-center", "app center", "appcenter", "lizenz", "license", "simulation", "simulator",
11
+ "expertenmodus", "expert mode", "xterm", "shell-konsole", "shell console", "zugänge & rechte",
12
+ "zugaenge & rechte", "access & rights", "user management", "benutzerverwaltung"
13
+ ];
14
+ const backupTokens = ["sicherung", "backup", "backitup"];
15
+ const dataPointTokens = ["datenpunkte", "datapoints", "objects", "objekte"];
16
+ const dangerousWriteTokens = [
17
+ "wert schreiben", "wert setzen", "schreiben", "write value", "set value", "setstate", "speichern",
18
+ "save", "anwenden", "apply", "löschen", "delete", "bearbeiten", "edit", "stift"
19
+ ];
20
+
21
+ const normalize = value => String(value == null ? "" : value).trim().toLowerCase();
22
+ const textOf = el => normalize([
23
+ el && el.textContent,
24
+ el && el.getAttribute && el.getAttribute("title"),
25
+ el && el.getAttribute && el.getAttribute("aria-label"),
26
+ el && el.getAttribute && el.getAttribute("data-testid"),
27
+ el && el.getAttribute && el.getAttribute("href"),
28
+ el && el.getAttribute && el.getAttribute("to"),
29
+ el && el.id,
30
+ el && el.className && typeof el.className === "string" ? el.className : ""
31
+ ].filter(Boolean).join(" "));
32
+
33
+ function classify(value) {
34
+ const text = normalize(value);
35
+ if (!text) return "unknown";
36
+ if (/^(?:system\.user\.)?admin(?:istrator)?$/.test(text) || /(?:^|[.:/_ -])admin(?:istrator)?(?:$|[.:/_ -])/.test(text)) return "admin";
37
+ if (/installer|installateur|inbetriebnahme/.test(text)) return "installer";
38
+ if (/endkunde|customer|smarthome|guest|gast|(?:^|\s)user(?:$|\s)/.test(text)) return "customer";
39
+ return "unknown";
40
+ }
41
+
42
+ function roleFromGlobals() {
43
+ const candidates = [
44
+ window.__NEXOWATT_ROLE__, window.__EOS_ROLE__, window.eosRole,
45
+ window.currentUser && window.currentUser.role,
46
+ window.user && window.user.role,
47
+ document.documentElement.getAttribute("data-role"),
48
+ document.body && document.body.getAttribute("data-role")
49
+ ];
50
+ for (const value of candidates) {
51
+ const role = classify(value);
52
+ if (role !== "unknown") return role;
53
+ }
54
+ return "unknown";
55
+ }
56
+
57
+ function roleFromStorage() {
58
+ const stores = [window.sessionStorage, window.localStorage];
59
+ const keys = ["eos.role", "eosRole", "role", "userRole", "nexowatt.role", "auth.role", "currentRole"];
60
+ for (const store of stores) {
61
+ try {
62
+ for (const key of keys) {
63
+ const role = classify(store.getItem(key));
64
+ if (role !== "unknown") return role;
65
+ }
66
+ } catch { /* storage can be disabled */ }
67
+ }
68
+ return "unknown";
69
+ }
70
+
71
+ function roleFromDom() {
72
+ const selectors = [
73
+ "[data-role]", "[data-user-role]", "[data-current-role]", "[aria-label*='Rolle' i]",
74
+ "[aria-label*='role' i]", "header", "nav", ".MuiAppBar-root"
75
+ ];
76
+ for (const selector of selectors) {
77
+ for (const el of document.querySelectorAll(selector)) {
78
+ const value = [
79
+ el.getAttribute && el.getAttribute("data-role"),
80
+ el.getAttribute && el.getAttribute("data-user-role"),
81
+ el.getAttribute && el.getAttribute("data-current-role"),
82
+ el.getAttribute && el.getAttribute("aria-label"),
83
+ el.textContent
84
+ ].filter(Boolean).join(" ");
85
+ const role = classify(value);
86
+ if (role !== "unknown") return role;
87
+ }
88
+ }
89
+ return "unknown";
90
+ }
91
+
92
+ function findDisplayedUser() {
93
+ const candidates = document.querySelectorAll("header *, nav *, .MuiAppBar-root *");
94
+ for (const el of candidates) {
95
+ const text = normalize(el.textContent);
96
+ if (!text || text.length > 80) continue;
97
+ if (/\b(admin|installer|user|guest|gast|endkunde)\b/.test(text)) return text;
98
+ }
99
+ return "";
100
+ }
101
+
102
+ async function fetchSecurityStatus() {
103
+ try {
104
+ const user = encodeURIComponent(findDisplayedUser());
105
+ const response = await fetch(`/api/nexowatt-eos/security/status?user=${user}&v=${VERSION}`, {
106
+ credentials: "same-origin", cache: "no-store", headers: { "Accept": "application/json" }
107
+ });
108
+ if (!response.ok) return null;
109
+ return await response.json();
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function isAdmin() { return state.role === "admin"; }
116
+ function isCustomer() { return state.role === "customer"; }
117
+
118
+ function hide(el, reason) {
119
+ if (!el || el === document.body || el === document.documentElement) return;
120
+ if (!el.hasAttribute(HIDDEN)) {
121
+ el.setAttribute(HIDDEN, reason || "restricted");
122
+ el.setAttribute("aria-hidden", "true");
123
+ el.style.setProperty("display", "none", "important");
124
+ }
125
+ }
126
+
127
+ function unhide(el) {
128
+ if (!el || !el.hasAttribute(HIDDEN)) return;
129
+ el.removeAttribute(HIDDEN);
130
+ el.removeAttribute("aria-hidden");
131
+ el.style.removeProperty("display");
132
+ }
133
+
134
+ function closestAction(el) {
135
+ return el && el.closest && el.closest("a,button,[role='button'],[role='tab'],li,.MuiButtonBase-root") || el;
136
+ }
137
+
138
+ function includesAny(text, tokens) {
139
+ return tokens.some(token => text.includes(token));
140
+ }
141
+
142
+ function applyNavigationPolicy() {
143
+ const actions = document.querySelectorAll("a,button,[role='button'],[role='tab'],li,.MuiButtonBase-root");
144
+ for (const raw of actions) {
145
+ const el = closestAction(raw);
146
+ const text = textOf(el);
147
+ if (!text) continue;
148
+
149
+ const adminOnly = includesAny(text, adminOnlyTokens) ||
150
+ (/\badmin\b/.test(text) && /(?:8081|adapter=admin|io.?broker)/.test(text));
151
+ const backup = includesAny(text, backupTokens);
152
+
153
+ if (!isAdmin() && adminOnly) hide(el, "admin-only");
154
+ else if (adminOnly) unhide(el);
155
+
156
+ if (backup && state.backupActive === false) hide(el, "backup-inactive");
157
+ else if (backup && state.backupActive === true && (isAdmin() || !adminOnly)) unhide(el);
158
+ }
159
+
160
+ if (!isAdmin()) {
161
+ for (const el of document.querySelectorAll("[data-testid*='expert' i],[class*='expert' i],[id*='expert' i],[title*='Experten' i],[aria-label*='Experten' i],[title*='Expert' i],[aria-label*='Expert' i]")) {
162
+ hide(closestAction(el), "admin-only-expert");
163
+ }
164
+ }
165
+ }
166
+
167
+ function isDataPointPage() {
168
+ const route = normalize(location.pathname + " " + location.hash + " " + document.title);
169
+ if (includesAny(route, dataPointTokens)) return true;
170
+ const selected = document.querySelector("[aria-selected='true'],.Mui-selected,.active");
171
+ return selected ? includesAny(textOf(selected), dataPointTokens) : false;
172
+ }
173
+
174
+ function makeDatapointsReadOnly() {
175
+ const root = document.querySelector("main") || document.body;
176
+ if (!isCustomer() || !isDataPointPage()) {
177
+ root && root.removeAttribute(READONLY);
178
+ return;
179
+ }
180
+ root.setAttribute(READONLY, "true");
181
+ for (const el of root.querySelectorAll("input,textarea,select,[contenteditable='true'],button,[role='button']")) {
182
+ const text = textOf(el);
183
+ const isSearch = /filter|suche|search|refresh|aktualisieren|reload|sort|spalte|column|zurück|back/.test(text) ||
184
+ /search/i.test(el.getAttribute && el.getAttribute("type") || "") ||
185
+ /filter|search/i.test(el.getAttribute && el.getAttribute("placeholder") || "");
186
+ const likelyWriter = includesAny(text, dangerousWriteTokens) ||
187
+ /value|wert|write|edit|save|delete|setstate|state-value/i.test(text) ||
188
+ (el.matches("input,textarea,select,[contenteditable='true']") && !isSearch);
189
+ if (likelyWriter && !isSearch) {
190
+ el.setAttribute("aria-disabled", "true");
191
+ el.setAttribute("tabindex", "-1");
192
+ if ("disabled" in el) el.disabled = true;
193
+ if (el.getAttribute("contenteditable") === "true") el.setAttribute("contenteditable", "false");
194
+ el.style.setProperty("pointer-events", "none", "important");
195
+ el.style.setProperty("opacity", "0.62", "important");
196
+ }
197
+ }
198
+ }
199
+
200
+ function currentRouteRestricted() {
201
+ const route = normalize(location.pathname + " " + location.search + " " + location.hash);
202
+ return includesAny(route, adminOnlyTokens);
203
+ }
204
+
205
+ function renderForbidden() {
206
+ if (document.getElementById("eos-role-forbidden")) return;
207
+ const overlay = document.createElement("div");
208
+ overlay.id = "eos-role-forbidden";
209
+ overlay.innerHTML = `<div class="eos-role-forbidden-card"><strong>Zugriff gesperrt</strong><span>App-Center, Lizenz, Simulation und Expertenmodus sind ausschließlich für Administratoren freigegeben.</span><button type="button">Zurück zum Cockpit</button></div>`;
210
+ overlay.querySelector("button").addEventListener("click", () => {
211
+ location.hash = "#/";
212
+ try { history.replaceState(null, "", location.pathname); } catch { /* ignore */ }
213
+ overlay.remove();
214
+ });
215
+ document.body.appendChild(overlay);
216
+ }
217
+
218
+ function guardCurrentRoute() {
219
+ if (!isAdmin() && currentRouteRestricted()) renderForbidden();
220
+ else document.getElementById("eos-role-forbidden")?.remove();
221
+ }
222
+
223
+ function clearExpertMode() {
224
+ if (isAdmin()) return;
225
+ const keys = ["expertMode", "expert-mode", "eos.expert", "eosExpertMode", "adminExpertMode"];
226
+ for (const store of [window.localStorage, window.sessionStorage]) {
227
+ try { keys.forEach(key => store.removeItem(key)); } catch { /* ignore */ }
228
+ }
229
+ window.__NEXOWATT_EXPERT_MODE__ = false;
230
+ }
231
+
232
+ function scrubPrivilegedUiSessions() {
233
+ if (isAdmin()) return;
234
+ const marker = "eos.nonAdminUiLogout.v7104";
235
+ try {
236
+ if (sessionStorage.getItem(marker)) return;
237
+ sessionStorage.setItem(marker, "1");
238
+ } catch { /* continue */ }
239
+ const protocol = location.protocol === "https:" ? "https:" : "http:";
240
+ const host = location.hostname;
241
+ for (const port of [8082, 8188]) {
242
+ const iframe = document.createElement("iframe");
243
+ iframe.hidden = true;
244
+ iframe.setAttribute("aria-hidden", "true");
245
+ iframe.src = `${protocol}//${host}:${port}/logout?source=eos-role-security&v=${VERSION}`;
246
+ document.body.appendChild(iframe);
247
+ setTimeout(() => iframe.remove(), 12000);
248
+ }
249
+ }
250
+
251
+ function apply() {
252
+ state.appliedAt = Date.now();
253
+ document.documentElement.setAttribute("data-eos-effective-role", state.role);
254
+ applyNavigationPolicy();
255
+ makeDatapointsReadOnly();
256
+ clearExpertMode();
257
+ guardCurrentRoute();
258
+ }
259
+
260
+ function installEventGuards() {
261
+ document.addEventListener("click", event => {
262
+ if (isAdmin()) return;
263
+ const action = closestAction(event.target);
264
+ const text = textOf(action);
265
+ if (includesAny(text, adminOnlyTokens) || (/\badmin\b/.test(text) && /(?:8081|adapter=admin|io.?broker)/.test(text))) {
266
+ event.preventDefault(); event.stopImmediatePropagation(); renderForbidden(); return;
267
+ }
268
+ if (isCustomer() && isDataPointPage() && includesAny(text, dangerousWriteTokens)) {
269
+ event.preventDefault(); event.stopImmediatePropagation(); return;
270
+ }
271
+ }, true);
272
+ document.addEventListener("submit", event => {
273
+ if (isCustomer() && isDataPointPage()) { event.preventDefault(); event.stopImmediatePropagation(); }
274
+ }, true);
275
+ for (const name of ["pushState", "replaceState"]) {
276
+ const original = history[name];
277
+ history[name] = function(...args) {
278
+ const result = original.apply(this, args);
279
+ queueMicrotask(apply);
280
+ return result;
281
+ };
282
+ }
283
+ addEventListener("hashchange", apply);
284
+ addEventListener("popstate", apply);
285
+ }
286
+
287
+ function injectStyles() {
288
+ if (document.getElementById("eos-role-security-style")) return;
289
+ const style = document.createElement("style");
290
+ style.id = "eos-role-security-style";
291
+ style.textContent = `
292
+ [${HIDDEN}] { display:none !important; }
293
+ #eos-role-forbidden { position:fixed; inset:0; z-index:2147483647; display:grid; place-items:center; padding:24px; background:rgba(1,17,28,.96); color:#eefaff; font-family:Arial,sans-serif; }
294
+ .eos-role-forbidden-card { width:min(620px,100%); display:grid; gap:18px; padding:30px; border:1px solid #16d49d; border-radius:18px; background:#072333; box-shadow:0 20px 80px rgba(0,0,0,.45); }
295
+ .eos-role-forbidden-card strong { color:#24e6ad; font-size:28px; }
296
+ .eos-role-forbidden-card span { line-height:1.55; }
297
+ .eos-role-forbidden-card button { justify-self:start; padding:10px 18px; border:1px solid #16d49d; border-radius:10px; color:#021911; background:#24e6ad; font-weight:700; cursor:pointer; }
298
+ [${READONLY}="true"]::before { content:"Nur-Lesezugriff für Endkunden"; position:sticky; top:0; z-index:10; display:block; margin:8px; padding:8px 12px; border:1px solid #1aa57d; border-radius:9px; background:#07342b; color:#cffff0; font:600 13px Arial,sans-serif; }
299
+ `;
300
+ document.head.appendChild(style);
301
+ }
302
+
303
+ async function init() {
304
+ injectStyles();
305
+ let role = roleFromGlobals();
306
+ if (role === "unknown") role = roleFromStorage();
307
+ if (role === "unknown") role = roleFromDom();
308
+ const status = await fetchSecurityStatus();
309
+ if (status) {
310
+ const serverRole = classify(status.role);
311
+ if (serverRole !== "unknown") role = serverRole;
312
+ if (typeof status.backupActive === "boolean") state.backupActive = status.backupActive;
313
+ state.user = status.user || "";
314
+ }
315
+ if (role === "unknown") role = "customer"; // least privilege until authenticated role is known
316
+ state.role = role;
317
+ try { sessionStorage.setItem("eos.role.effective", role); } catch { /* ignore */ }
318
+ installEventGuards();
319
+ apply();
320
+ scrubPrivilegedUiSessions();
321
+ const observer = new MutationObserver(() => {
322
+ clearTimeout(observer._timer);
323
+ observer._timer = setTimeout(apply, 40);
324
+ });
325
+ observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "aria-selected", "href"] });
326
+ setInterval(async () => {
327
+ const refreshed = await fetchSecurityStatus();
328
+ if (refreshed && typeof refreshed.backupActive === "boolean" && refreshed.backupActive !== state.backupActive) {
329
+ state.backupActive = refreshed.backupActive; apply();
330
+ }
331
+ }, 10000);
332
+ }
333
+
334
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true });
335
+ else void init();
336
+ })();
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const root = path.resolve(__dirname, '..');
7
+ const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
8
+ const json = relative => JSON.parse(read(relative));
9
+ const fail = message => {
10
+ console.error(`[NexoWatt EOS auto-update placement] ${message}`);
11
+ process.exit(1);
12
+ };
13
+ const must = (value, message) => value || fail(message);
14
+
15
+ const sourceJs = read('src-admin/public/js/eos-auto-update.js');
16
+ const builtJs = read('adminWww/js/eos-auto-update.js');
17
+ const sourceCss = read('src-admin/public/css/eos-auto-update.css');
18
+ const builtCss = read('adminWww/css/eos-auto-update.css');
19
+ const index = read('adminWww/index.html');
20
+ const info = json('NEXOWATT_EOS_BUILD_INFO.json');
21
+ const cacheTag = String(info.autoUpdateCacheTag || info.autoUpdateCacheVersion || info.shellCacheTag || info.shellCacheVersion);
22
+
23
+ must(sourceJs === builtJs, 'JavaScript source/build drift');
24
+ must(sourceCss === builtCss, 'CSS source/build drift');
25
+ must(sourceJs.includes("const VERSION = 'v7103-system-settings-only'"), 'v7103 placement marker missing');
26
+ must(sourceJs.includes('[role="dialog"][aria-labelledby="system-settings-dialog-title"]'), 'System Settings dialog selector missing');
27
+ must(sourceJs.includes("root.dataset.context = 'system-settings'"), 'System Settings context marker missing');
28
+ must(sourceJs.includes("root.setAttribute('data-eos-system-settings-only', 'true')"), 'system-settings-only marker missing');
29
+ must(sourceJs.includes('mount.content.insertBefore(root, mount.appBar.nextSibling)'), 'card is not inserted below the System Settings app bar');
30
+ must(sourceJs.includes("location: 'system-settings'"), 'public UI location contract missing');
31
+
32
+ for (const forbidden of [
33
+ 'introActive',
34
+ 'eos-native-overview-hero',
35
+ 'eos-ems-overview-runtime',
36
+ "document.getElementById('app-paper')",
37
+ ]) {
38
+ must(!sourceJs.includes(forbidden), `global page placement remains active: ${forbidden}`);
39
+ }
40
+
41
+ must(index.includes(`eos-auto-update.js?v=${cacheTag}`), `auto-update JavaScript cache key ${cacheTag} missing`);
42
+ must(index.includes(`eos-auto-update.css?v=${cacheTag}`), `auto-update CSS cache key ${cacheTag} missing`);
43
+ must(sourceCss.includes('visible only inside the System Settings dialog'), 'CSS ownership comment missing');
44
+ must(sourceCss.includes('[data-context="system-settings"]'), 'System Settings CSS scope marker missing');
45
+
46
+ console.log('[NexoWatt EOS auto-update placement] OK (System Settings only; no Overview/Modules injection)');
@@ -16,7 +16,7 @@ for (const file of [
16
16
  'MERGE_UPDATE.ps1',
17
17
  'MERGE_UPDATE.cmd',
18
18
  'MERGE_UPDATE.sh',
19
- 'MERGE_UPDATE_README_V7.10.2.md',
19
+ 'MERGE_UPDATE_README_V7.10.4.md',
20
20
  'tools/nexowatt-apply-ems-overview.cjs',
21
21
  'tools/nexowatt-sync-release-version.cjs',
22
22
  'tools/nexowatt-version-sync-selftest.cjs',
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ const assert = require("node:assert/strict");
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const root = path.resolve(__dirname, "..");
6
+ const securityFile = path.join(root, "build", "lib", "eosRoleSecurity.js");
7
+ assert.ok(fs.existsSync(securityFile), "build/lib/eosRoleSecurity.js is missing");
8
+ const security = require(securityFile);
9
+ assert.equal(security.classifyRole("admin"), "admin");
10
+ assert.equal(security.classifyRole("Administrator"), "admin");
11
+ assert.equal(security.classifyRole("installer"), "installer");
12
+ assert.equal(security.classifyRole("Gast / Endkunde"), "customer");
13
+ assert.equal(security.isAdminOnlyPath("/#/app-center"), true);
14
+ assert.equal(security.isAdminOnlyPath("/license"), true);
15
+ assert.equal(security.isAdminOnlyPath("/simulation"), true);
16
+ assert.equal(security.isAdminOnlyPath("/cockpit"), false);
17
+ assert.equal(security.isRightsPath("/#/users-and-groups"), true);
18
+ assert.equal(security.isRightsPath("/cockpit"), false);
19
+ const htmlFiles = [];
20
+ (function walk(dir) {
21
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
22
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
23
+ const full = path.join(dir, entry.name);
24
+ if (entry.isDirectory()) walk(full);
25
+ else if (entry.name === "index.html") htmlFiles.push(full);
26
+ }
27
+ })(root);
28
+ const runtimeHtml = htmlFiles.filter(file => /<script[^>]+(?:assets\/|src=)/i.test(fs.readFileSync(file, "utf8")));
29
+ assert.ok(runtimeHtml.length > 0, "No runtime index.html was found");
30
+ for (const file of runtimeHtml) {
31
+ const html = fs.readFileSync(file, "utf8");
32
+ assert.match(html, /nexowatt-role-security\.js\?v=7104/, `Role guard missing in ${path.relative(root, file)}`);
33
+ assert.ok(fs.existsSync(path.join(path.dirname(file), "nexowatt-role-security.js")), `Client guard missing next to ${path.relative(root, file)}`);
34
+ }
35
+ const main = fs.readFileSync(path.join(root, "build", "main.js"), "utf8");
36
+ assert.match(main, /eosRoleSecurity/);
37
+ assert.match(main, /installHttpGuard/);
38
+ assert.match(main, /enforceRoleAcls/);
39
+ const client = runtimeHtml.length ? fs.readFileSync(path.join(path.dirname(runtimeHtml[0]), "nexowatt-role-security.js"), "utf8") : "";
40
+ for (const token of ["app-center", "lizenz", "simulation", "expertenmodus", "xterm", "Nur-Lesezugriff", "backupActive"]) {
41
+ assert.ok(client.toLowerCase().includes(token.toLowerCase()), `Client guard is missing ${token}`);
42
+ }
43
+ console.log("[NexoWatt EOS role security] OK");