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,310 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * NexoWatt EOS role security boundary.
5
+ *
6
+ * This module intentionally uses only Node.js core APIs so the prebuilt
7
+ * repository can be packed and published without a local node_modules folder.
8
+ */
9
+ const http = require("node:http");
10
+ const https = require("node:https");
11
+ const { URL } = require("node:url");
12
+
13
+ let adapterRef = null;
14
+ let installed = false;
15
+ let backupCache = { value: false, expires: 0 };
16
+
17
+ const ADMIN_NAMES = new Set(["admin", "administrator", "system.user.admin"]);
18
+ const INSTALLER_TOKENS = ["installer", "installateur", "inbetriebnahme"];
19
+ const CUSTOMER_TOKENS = ["user", "guest", "gast", "endkunde", "customer", "smarthome"];
20
+
21
+ const ADMIN_ONLY_PATHS = [
22
+ /(?:^|[\\/#?&_.-])app[\\s_.-]*center(?:$|[\\/#?&_.-])/i,
23
+ /(?:^|[\\/#?&_.-])lizenz(?:en)?(?:$|[\\/#?&_.-])/i,
24
+ /(?:^|[\\/#?&_.-])licen[cs]e(?:s)?(?:$|[\\/#?&_.-])/i,
25
+ /(?:^|[\\/#?&_.-])simulation(?:$|[\\/#?&_.-])/i,
26
+ /(?:^|[\\/#?&_.-])simulator(?:$|[\\/#?&_.-])/i,
27
+ /(?:^|[\\/#?&_.-])experten?(?:modus)?(?:$|[\\/#?&_.-])/i,
28
+ /(?:^|[\\/#?&_.-])expert(?:mode)?(?:$|[\\/#?&_.-])/i,
29
+ /(?:^|[\\/#?&_.-])xterm(?:$|[\\/#?&_.-])/i,
30
+ ];
31
+
32
+ const RIGHTS_PATHS = [
33
+ /(?:^|[\\/#?&_.-])users?(?:$|[\\/#?&_.-])/i,
34
+ /(?:^|[\\/#?&_.-])groups?(?:$|[\\/#?&_.-])/i,
35
+ /zug[aä]nge/i,
36
+ /rechte/i,
37
+ /permissions?/i,
38
+ /password/i,
39
+ /passwort/i,
40
+ ];
41
+
42
+ function normalize(value) {
43
+ if (value === null || value === undefined) return "";
44
+ if (typeof value === "object") {
45
+ if (typeof value.name === "string") return value.name.trim().toLowerCase();
46
+ if (typeof value.user === "string") return value.user.trim().toLowerCase();
47
+ return "";
48
+ }
49
+ return String(value).trim().toLowerCase();
50
+ }
51
+
52
+ function classifyRole(value) {
53
+ const text = normalize(value);
54
+ if (!text) return "unknown";
55
+ if (ADMIN_NAMES.has(text) || /(?:^|[.:/_-])admin(?:istrator)?(?:$|[.:/_-])/.test(text)) return "admin";
56
+ if (INSTALLER_TOKENS.some(token => text.includes(token))) return "installer";
57
+ if (CUSTOMER_TOKENS.some(token => text === token || text.includes(token))) return "customer";
58
+ return "unknown";
59
+ }
60
+
61
+ function parseBasicUser(header) {
62
+ if (!header || !/^basic\s+/i.test(header)) return "";
63
+ try {
64
+ const decoded = Buffer.from(header.replace(/^basic\s+/i, ""), "base64").toString("utf8");
65
+ return decoded.split(":", 1)[0] || "";
66
+ } catch {
67
+ return "";
68
+ }
69
+ }
70
+
71
+ function parseCookieHeader(header) {
72
+ const out = Object.create(null);
73
+ String(header || "").split(";").forEach(part => {
74
+ const idx = part.indexOf("=");
75
+ if (idx <= 0) return;
76
+ const key = part.slice(0, idx).trim();
77
+ let value = part.slice(idx + 1).trim();
78
+ try { value = decodeURIComponent(value); } catch { /* ignore */ }
79
+ out[key] = value;
80
+ });
81
+ return out;
82
+ }
83
+
84
+ function extractIdentity(req) {
85
+ const candidates = [];
86
+ if (req) {
87
+ candidates.push(req.user, req.username, req.auth && req.auth.user, req.session && req.session.user,
88
+ req.session && req.session.username, req.session && req.session.passport && req.session.passport.user,
89
+ req.ioBrokerUser, req._user);
90
+ const headers = req.headers || {};
91
+ candidates.push(headers["x-iobroker-user"], headers["x-eos-user"], headers["x-authenticated-user"],
92
+ headers["x-forwarded-user"], parseBasicUser(headers.authorization));
93
+ const cookies = parseCookieHeader(headers.cookie);
94
+ for (const key of ["iobroker.user", "iobroker_user", "eos.user", "eosUser", "user", "username", "authUser"]) {
95
+ candidates.push(cookies[key]);
96
+ }
97
+ try {
98
+ const parsed = new URL(req.url || "/", "http://127.0.0.1");
99
+ candidates.push(parsed.searchParams.get("user"), parsed.searchParams.get("username"));
100
+ } catch { /* ignore */ }
101
+ }
102
+ for (const value of candidates) {
103
+ if (normalize(value)) return normalize(value);
104
+ }
105
+ return "";
106
+ }
107
+
108
+ function roleFromRequest(req) {
109
+ const explicitRole = normalize(req && req.headers && (req.headers["x-eos-role"] || req.headers["x-nexowatt-role"]));
110
+ const role = classifyRole(explicitRole);
111
+ if (role !== "unknown") return role;
112
+ return classifyRole(extractIdentity(req));
113
+ }
114
+
115
+ function isAdminOnlyPath(rawUrl) {
116
+ const value = String(rawUrl || "");
117
+ return ADMIN_ONLY_PATHS.some(pattern => pattern.test(value));
118
+ }
119
+
120
+ function isRightsPath(rawUrl) {
121
+ const value = String(rawUrl || "");
122
+ return RIGHTS_PATHS.some(pattern => pattern.test(value));
123
+ }
124
+
125
+ function isStateWriteRequest(req) {
126
+ const method = String(req && req.method || "GET").toUpperCase();
127
+ if (!["POST", "PUT", "PATCH", "DELETE"].includes(method)) return false;
128
+ const value = String(req && req.url || "");
129
+ return /(?:setstate|write|objects?\/|states?\/|sendto|command)/i.test(value);
130
+ }
131
+
132
+ function deny(res, message, statusCode = 403) {
133
+ if (!res || res.headersSent || res.writableEnded) return true;
134
+ const accepts = String((res.req && res.req.headers && res.req.headers.accept) || "");
135
+ const json = accepts.includes("application/json") || String(res.req && res.req.url || "").includes("/api/");
136
+ res.statusCode = statusCode;
137
+ res.setHeader("Cache-Control", "no-store");
138
+ res.setHeader("X-Content-Type-Options", "nosniff");
139
+ if (json) {
140
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
141
+ res.end(JSON.stringify({ ok: false, error: "forbidden", message }));
142
+ } else {
143
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
144
+ res.end(`<!doctype html><html lang="de"><meta charset="utf-8"><title>Zugriff gesperrt</title><style>body{margin:0;background:#031724;color:#eaf6ff;font:16px Arial,sans-serif;display:grid;place-items:center;min-height:100vh}.box{max-width:620px;padding:32px;border:1px solid #0dbb87;border-radius:18px;background:#071f2f}h1{margin-top:0;color:#24e6ad}</style><div class="box"><h1>Zugriff gesperrt</h1><p>${message}</p></div></html>`);
145
+ }
146
+ return true;
147
+ }
148
+
149
+ async function getBackupActive() {
150
+ const now = Date.now();
151
+ if (backupCache.expires > now) return backupCache.value;
152
+ let active = false;
153
+ const adapter = adapterRef;
154
+ if (adapter) {
155
+ try {
156
+ let rows = [];
157
+ if (typeof adapter.getObjectViewAsync === "function") {
158
+ const result = await adapter.getObjectViewAsync("system", "instance", { startkey: "system.adapter.", endkey: "system.adapter.\u9999" });
159
+ rows = result && Array.isArray(result.rows) ? result.rows : [];
160
+ } else if (typeof adapter.getObjectListAsync === "function") {
161
+ const result = await adapter.getObjectListAsync({ startkey: "system.adapter.", endkey: "system.adapter.\u9999" });
162
+ rows = result && Array.isArray(result.rows) ? result.rows : [];
163
+ }
164
+ active = rows.some(row => {
165
+ const obj = row && (row.value || row.doc);
166
+ const id = normalize(row && row.id || obj && obj._id);
167
+ return /system\.adapter\.(?:backitup|backup|nexowatt-backup|eos-backup)\./.test(id) && Boolean(obj && obj.common && obj.common.enabled);
168
+ });
169
+ } catch (error) {
170
+ if (adapter && adapter.log && typeof adapter.log.debug === "function") {
171
+ adapter.log.debug(`[role-security] Backup status could not be read: ${error && error.message || error}`);
172
+ }
173
+ }
174
+ }
175
+ backupCache = { value: active, expires: now + 5000 };
176
+ return active;
177
+ }
178
+
179
+ function attachAdapter(adapter) {
180
+ adapterRef = adapter || null;
181
+ backupCache.expires = 0;
182
+ }
183
+
184
+ function cloneAclSection(section) {
185
+ return section && typeof section === "object" ? { ...section } : {};
186
+ }
187
+
188
+ async function enforceRoleAcls(adapter = adapterRef) {
189
+ if (!adapter || typeof adapter.getForeignObjectAsync !== "function" || typeof adapter.setForeignObjectAsync !== "function") return;
190
+ const ids = ["system.group.endkunde"];
191
+ for (const id of ids) {
192
+ try {
193
+ const obj = await adapter.getForeignObjectAsync(id);
194
+ if (!obj || !obj.common) continue;
195
+ const acl = obj.common.acl && typeof obj.common.acl === "object" ? { ...obj.common.acl } : {};
196
+ const objectAcl = cloneAclSection(acl.object);
197
+ const stateAcl = cloneAclSection(acl.state);
198
+ const usersAcl = cloneAclSection(acl.users);
199
+ const fileAcl = cloneAclSection(acl.file);
200
+ const otherAcl = cloneAclSection(acl.other);
201
+ const before = JSON.stringify({ objectAcl, stateAcl, usersAcl, fileAcl, otherAcl });
202
+ Object.assign(objectAcl, { list: true, read: true, write: false, delete: false });
203
+ Object.assign(stateAcl, { list: true, read: true, write: false, delete: false, create: false });
204
+ Object.assign(usersAcl, { write: false, delete: false, create: false });
205
+ Object.assign(fileAcl, { list: true, read: true, write: false, delete: false, create: false });
206
+ Object.assign(otherAcl, { execute: false, sendto: false });
207
+ acl.object = objectAcl;
208
+ acl.state = stateAcl;
209
+ acl.users = usersAcl;
210
+ acl.file = fileAcl;
211
+ acl.other = otherAcl;
212
+ const after = JSON.stringify({ objectAcl, stateAcl, usersAcl, fileAcl, otherAcl });
213
+ if (before !== after) {
214
+ obj.common.acl = acl;
215
+ await adapter.setForeignObjectAsync(id, obj);
216
+ if (adapter.log && typeof adapter.log.info === "function") {
217
+ adapter.log.info("[role-security] End customer group enforced as read-only for objects and states");
218
+ }
219
+ }
220
+ } catch (error) {
221
+ if (adapter.log && typeof adapter.log.warn === "function") {
222
+ adapter.log.warn(`[role-security] ACL enforcement failed for ${id}: ${error && error.message || error}`);
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ async function handleStatus(req, res) {
229
+ const role = roleFromRequest(req);
230
+ const backupActive = await getBackupActive();
231
+ res.statusCode = 200;
232
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
233
+ res.setHeader("Cache-Control", "no-store");
234
+ res.setHeader("X-Content-Type-Options", "nosniff");
235
+ res.end(JSON.stringify({
236
+ ok: true,
237
+ role,
238
+ user: extractIdentity(req) || null,
239
+ backupActive,
240
+ capabilities: {
241
+ appCenter: role === "admin",
242
+ license: role === "admin",
243
+ simulation: role === "admin",
244
+ expertMode: role === "admin",
245
+ accessRights: role === "admin",
246
+ xterm: role === "admin",
247
+ ioBrokerAdmin: role === "admin",
248
+ writeDatapoints: role === "admin" || role === "installer",
249
+ readDatapoints: true,
250
+ },
251
+ }));
252
+ }
253
+
254
+ function wrapListener(listener) {
255
+ if (typeof listener !== "function") return listener;
256
+ return function guardedListener(req, res) {
257
+ try {
258
+ const url = String(req && req.url || "/");
259
+ if (/^\/api\/nexowatt-eos\/security\/(?:status|me)(?:[/?#]|$)/i.test(url)) {
260
+ void handleStatus(req, res);
261
+ return;
262
+ }
263
+ const role = roleFromRequest(req);
264
+ if (role !== "unknown" && role !== "admin" && (isAdminOnlyPath(url) || isRightsPath(url))) {
265
+ deny(res, "Diese Funktion ist ausschließlich für Administratoren freigegeben.");
266
+ return;
267
+ }
268
+ if (role === "customer" && isStateWriteRequest(req)) {
269
+ deny(res, "Endkunden besitzen für Datenpunkte ausschließlich Leserechte.");
270
+ return;
271
+ }
272
+ } catch {
273
+ // Fail closed only for positively identified restricted users. Other
274
+ // requests continue to the existing server so startup is never blocked.
275
+ }
276
+ return listener.apply(this, arguments);
277
+ };
278
+ }
279
+
280
+ function patchCreateServer(module) {
281
+ const original = module.createServer;
282
+ if (typeof original !== "function" || original.__nexowattRoleSecurity) return;
283
+ function secureCreateServer(...args) {
284
+ if (typeof args[0] === "function") args[0] = wrapListener(args[0]);
285
+ else if (typeof args[1] === "function") args[1] = wrapListener(args[1]);
286
+ return original.apply(this, args);
287
+ }
288
+ Object.defineProperty(secureCreateServer, "__nexowattRoleSecurity", { value: true });
289
+ module.createServer = secureCreateServer;
290
+ }
291
+
292
+ function installHttpGuard() {
293
+ if (installed) return;
294
+ installed = true;
295
+ patchCreateServer(http);
296
+ patchCreateServer(https);
297
+ }
298
+
299
+ module.exports = {
300
+ attachAdapter,
301
+ classifyRole,
302
+ enforceRoleAcls,
303
+ extractIdentity,
304
+ getBackupActive,
305
+ installHttpGuard,
306
+ isAdminOnlyPath,
307
+ isRightsPath,
308
+ roleFromRequest,
309
+ wrapListener,
310
+ };
package/build/main.js CHANGED
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+
3
+ const eosRoleSecurity = require("./lib/eosRoleSecurity");
4
+ eosRoleSecurity.installHttpGuard();
2
5
  /**
3
6
  * Admin backend
4
7
  *
@@ -329,6 +332,8 @@ class Admin extends adapter_core_1.Adapter {
329
332
  * Is called when databases are connected and adapter received configuration.
330
333
  */
331
334
  onReady = async () => {
335
+ eosRoleSecurity.attachAdapter(this);
336
+ await eosRoleSecurity.enforceRoleAcls(this);
332
337
  const systemConfig = await this.getForeignObjectAsync('system.config');
333
338
  if (systemConfig) {
334
339
  systemConfig.native = systemConfig.native || {};
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "eos-admin",
4
- "version": "7.10.3",
4
+ "version": "7.10.4",
5
5
  "titleLang": {
6
6
  "en": "NexoWatt EOS Admin",
7
7
  "de": "NexoWatt EOS Admin",
@@ -18,10 +18,10 @@
18
18
  "connectionType": "local",
19
19
  "dataSource": "push",
20
20
  "news": {
21
- "7.10.3": {
22
- "de": "Stable 7.10.3 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.3 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.3 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."
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
25
  },
26
26
  "7.10.2": {
27
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.",
@@ -349,7 +349,7 @@
349
349
  "icon": "admin.svg",
350
350
  "messagebox": true,
351
351
  "enabled": true,
352
- "extIcon": "https://unpkg.com/iobroker.eos-admin@7.10.3/admin/admin.svg",
352
+ "extIcon": "https://unpkg.com/iobroker.eos-admin@7.10.4/admin/admin.svg",
353
353
  "keywords": [
354
354
  "NexoWatt",
355
355
  "EOS",
@@ -360,7 +360,7 @@
360
360
  "licensed"
361
361
  ],
362
362
  "compact": true,
363
- "readme": "https://unpkg.com/iobroker.eos-admin@7.10.3/README.md",
363
+ "readme": "https://unpkg.com/iobroker.eos-admin@7.10.4/README.md",
364
364
  "authors": [
365
365
  "NexoWatt"
366
366
  ],
@@ -428,7 +428,7 @@
428
428
  "nondeletable": false,
429
429
  "allowAdapterUpdate": true,
430
430
  "allowAdapterDelete": false,
431
- "meta": "https://unpkg.com/iobroker.eos-admin@7.10.3/io-package.json",
431
+ "meta": "https://unpkg.com/iobroker.eos-admin@7.10.4/io-package.json",
432
432
  "npmPackage": "iobroker.eos-admin",
433
433
  "packetName": "iobroker.eos-admin",
434
434
  "publisher": "NexoWatt"
@@ -1188,5 +1188,5 @@
1188
1188
  }
1189
1189
  }
1190
1190
  },
1191
- "version": "7.10.3"
1191
+ "version": "7.10.4"
1192
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.3",
4
+ "version": "7.10.4",
5
5
  "contributors": [
6
6
  "bluefox <dogafox@gmail.com>",
7
7
  "apollon77",
@@ -133,13 +133,15 @@
133
133
  "NEXOWATT_EOS_PREBUILT_MANIFEST.json",
134
134
  "tools/nexowatt-prebuilt-release-selftest.cjs",
135
135
  "tools/nexowatt-stable-v7103-selftest.cjs",
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",
141
- "MERGE_UPDATE_README_V7.10.3.md",
142
- "tools/nexowatt-auto-update-placement-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"
143
145
  ],
144
146
  "scripts": {
145
147
  "npm": "npm i -f && cd src-admin && npm i -f",
@@ -174,7 +176,7 @@
174
176
  "nexowatt:repo-entry": "node tools/nexowatt-generate-repo-entry.cjs",
175
177
  "nexowatt:patch-repo": "node tools/nexowatt-patch-repo.cjs",
176
178
  "nexowatt:repair-update": "node tools/nexowatt-repair-eos-admin-update.cjs",
177
- "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",
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",
178
180
  "nexowatt:patch-built-frontend": "node tools/nexowatt-patch-built-frontend.cjs",
179
181
  "clean:eos-runtime": "node tools/nexowatt-clean-legacy-runtime.cjs",
180
182
  "precheck:eos-package": "npm run sync:eos-version && npm run prepare:eos-release-defaults && npm run clean:eos-runtime",
@@ -202,7 +204,8 @@
202
204
  "check:eos-prebuilt-release": "node tools/nexowatt-prebuilt-release-selftest.cjs",
203
205
  "seal:eos-prebuilt-release": "node tools/nexowatt-prebuilt-release-selftest.cjs --write",
204
206
  "check:eos-stable-v7103": "node tools/nexowatt-stable-v7103-selftest.cjs",
205
- "test:eos-auto-update-placement": "node tools/nexowatt-auto-update-placement-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"
206
209
  },
207
210
  "author": "NexoWatt",
208
211
  "private": false,
@@ -213,7 +216,7 @@
213
216
  },
214
217
  "nexowattReleasePolicy": {
215
218
  "distTag": "latest",
216
- "purpose": "accepted stable NexoWatt EOS sales-product candidate 7.10.3"
219
+ "purpose": "accepted stable NexoWatt EOS sales-product candidate 7.10.4"
217
220
  },
218
221
  "publisher": "NexoWatt"
219
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 |