iobroker.eos-admin 7.10.3 → 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.
@@ -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
+ })();
@@ -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.3.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");
@@ -31,7 +31,7 @@ const autoUi = read('adminWww/js/eos-auto-update.js');
31
31
  const autoCss = read('adminWww/css/eos-auto-update.css');
32
32
  const autoTag = String(info.autoUpdateCacheTag || info.autoUpdateCacheVersion || info.shellCacheTag || info.shellCacheVersion);
33
33
 
34
- must(pkg.version === '7.10.3' && io.version === '7.10.3' && io.common.version === '7.10.3' && info.version === '7.10.3', 'version drift');
34
+ must(pkg.version === '7.10.4' && io.version === '7.10.4' && io.common.version === '7.10.4' && info.version === '7.10.4', 'version drift');
35
35
  must(io.native.eosRequireFirstLoginPassword === false && io.native.eosRequireFirstLoginPasswordChange === false, 'forced first password still enabled');
36
36
  must(!index.includes('eos-account-management.js'), 'custom reset UI still loaded');
37
37
  must(!boot.includes('installIntegratedFirstLogin(base);') && !boot.includes('showFirstLoginPassword(resolved, base);'), 'forced first-login UI still active');
@@ -49,7 +49,7 @@ must(!autoBackend.includes('extendForeignObjectAsync(this.instanceId'), 'runtime
49
49
  must(web.includes('this.nexowattStableUpdateManager?.stop();'), 'auto-update manager is not stopped with the web server');
50
50
  must(fs.existsSync(path.join(root, 'build/lib/eosRequestSecurity.js')), 'eosRequestSecurity runtime module missing');
51
51
 
52
- // 7.10.3 placement acceptance: System Settings only.
52
+ // 7.10.4 placement acceptance: System Settings only.
53
53
  must(index.includes(`eos-auto-update.js?v=${autoTag}`), 'auto-update JavaScript cache key missing');
54
54
  must(index.includes(`eos-auto-update.css?v=${autoTag}`), 'auto-update CSS cache key missing');
55
55
  must(autoUi.includes('[role="dialog"][aria-labelledby="system-settings-dialog-title"]'), 'System Settings selector missing');
@@ -133,14 +133,14 @@ for (const file of [
133
133
  'LICENSE',
134
134
  'NEXOWATT_PROPRIETARY_LICENSE.md',
135
135
  'THIRD_PARTY_NOTICES.md',
136
- 'README_STABILITY_V7.10.3.md',
137
- 'RELEASE_NOTES_V7.10.3.md',
138
- 'PUBLISH_STABLE_V7.10.3.md',
139
- 'INSTALL_TEST_V7.10.3.md',
140
- 'RELEASE_ACCEPTANCE_V7.10.3.md',
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
141
  'MERGE_UPDATE.ps1',
142
142
  'MERGE_UPDATE.cmd',
143
- 'MERGE_UPDATE_README_V7.10.3.md',
143
+ 'MERGE_UPDATE_README_V7.10.4.md',
144
144
  'tools/nexowatt-patch-built-frontend.cjs',
145
145
  'tools/nexowatt-native-shell-selftest.cjs',
146
146
  'tools/nexowatt-clean-legacy-runtime.cjs',
@@ -1,7 +0,0 @@
1
- # Installationstest 7.10.3
2
-
3
- 1. Version 7.10.3 installieren und EOS Admin starten.
4
- 2. Mindestens 60 Sekunden prüfen, dass kein Start-/Stop-Loop auftritt.
5
- 3. Übersicht und Module öffnen: Die Karte **Automatische Adapter-Updates** darf dort nicht sichtbar sein.
6
- 4. Schraubenschlüssel öffnen: Die Karte muss direkt unter der Kopfleiste der Systemeinstellungen erscheinen.
7
- 5. Schalter einmal deaktivieren und wieder aktivieren; Status und Repository müssen weiter angezeigt werden.
@@ -1,3 +0,0 @@
1
- # Merge-/Update-Hinweis 7.10.3
2
-
3
- Für einen reproduzierbaren Stand den bisherigen Arbeitsordner vollständig ersetzen oder die Repository-ZIP in einen leeren Ordner entpacken. Danach `npm publish --dry-run` ausführen. Eine Vermischung mit älteren `adminWww/js/eos-auto-update.js`-Dateien würde die alte globale Platzierung wieder aktivieren und wird durch die Selbsttests blockiert.
@@ -1,5 +0,0 @@
1
- # NexoWatt EOS Admin 7.10.3 – Stabilitätsstand
2
-
3
- Version 7.10.3 übernimmt die validierten Backend-, Paket- und Restart-Schutzmaßnahmen aus 7.10.2 unverändert. Die sichtbare Karte **Automatische Adapter-Updates** wird ausschließlich im Dialog **Systemeinstellungen** hinter dem Schraubenschlüssel eingebunden. Sie darf weder in der Übersicht noch in Module, Dienste, Datenpunkte oder anderen Hauptseiten erscheinen.
4
-
5
- Die Browserdateien `src-admin/public/js/eos-auto-update.js` und `adminWww/js/eos-auto-update.js` sowie die zugehörigen CSS-Dateien müssen identisch sein. Der Cache-Schlüssel `7103` erzwingt nach dem Update das Laden der neuen Platzierungslogik.
@@ -1,3 +0,0 @@
1
- # Release Acceptance 7.10.3
2
-
3
- Akzeptiert, wenn Paketvalidierung, Stabilitätstests, Prebuilt-Manifest, Backend-Runtime, Publish-Dry-Run und der Auto-Update-Placement-Test erfolgreich sind. Die Update-Karte ist ausschließlich im System-Settings-Dialog zulässig.
@@ -1,7 +0,0 @@
1
- # Release Notes 7.10.3
2
-
3
- - Automatische NexoWatt-Adapter-Updates aus dem normalen Seiteninhalt entfernt.
4
- - Update-Karte ausschließlich in **Systemeinstellungen** hinter dem Schraubenschlüssel platziert.
5
- - Keine Änderung an Stable-Repository, verwalteten Adaptern oder Update-Backendlogik.
6
- - Eigener Browser-Cache-Schlüssel `7103` verhindert die Verwendung der alten Platzierung.
7
- - Neuer Placement-Selbsttest verhindert eine spätere Rückkehr auf Übersicht oder Module.