iobroker.eos-admin 7.9.47 → 7.9.49
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/admin/jsonConfig.json5 +70 -0
- package/adminWww/assets/Objects-DPan0bzw.js +1 -1
- package/adminWww/assets/bootstrap-COulQZax.js +1 -1
- package/adminWww/assets/index-D2ymscJA.js +1 -1
- package/adminWww/css/eos-branding.css +77 -0
- package/adminWww/index.html +6 -5
- package/adminWww/js/eos-branding.js +8 -7
- package/adminWww/js/eos-objects-state-tools.js +75 -69
- package/adminWww/js/eos-role-ui.js +304 -0
- package/adminWww/js/eos-security-ui.js +1 -1
- package/build/lib/web.js +123 -8
- package/docs/NEXOWATT_EOS_BRANDING_DE.md +29 -29
- package/docs/NEXOWATT_EOS_UI_LOGO_LOGIN_V6_DE.md +9 -9
- package/docs/NEXOWATT_EOS_UI_PERFORMANCE_V5_DE.md +25 -25
- package/docs/NEXOWATT_EOS_UI_V12_HEADER_LOGO_NAV_CLEANUP_DE.md +16 -16
- package/docs/NEXOWATT_EOS_UI_V12_HEADER_NAV_LOGO_FIX_DE.md +16 -16
- package/docs/NEXOWATT_EOS_UI_V15_DE.md +8 -8
- package/docs/NEXOWATT_EOS_UI_V16_DE.md +9 -9
- package/docs/NEXOWATT_EOS_UI_V17_DE.md +24 -24
- package/docs/NEXOWATT_EOS_UI_V6_FIXES_DE.md +13 -13
- package/docs/NEXOWATT_EOS_UI_V6_LOGIN_LOGO_404_FIX_DE.md +9 -9
- package/docs/NEXOWATT_EOS_UI_V6_LOGO_LOGIN_FIX_DE.md +10 -10
- package/docs/NEXOWATT_EOS_UI_V7_RESTORE_LOGO_SCOPE_FIX_DE.md +18 -18
- package/io-package.json +32 -5
- package/package.json +1 -1
package/adminWww/index.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
rel="stylesheet"
|
|
32
32
|
href="css/leaflet.css"
|
|
33
33
|
/>
|
|
34
|
-
<link rel="stylesheet" href="./css/eos-branding.css?v=
|
|
34
|
+
<link rel="stylesheet" href="./css/eos-branding.css?v=49" />
|
|
35
35
|
<link
|
|
36
36
|
rel="manifest"
|
|
37
37
|
href="manifest.json"
|
|
@@ -154,10 +154,11 @@
|
|
|
154
154
|
<script type="module" crossorigin src="./assets/index-CQZugZ1z.js"></script>
|
|
155
155
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js">
|
|
156
156
|
<link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js">
|
|
157
|
-
<script defer src="./js/eos-branding.js?v=
|
|
158
|
-
<script defer src="./js/eos-security-ui.js?v=
|
|
159
|
-
<script defer src="./js/eos-
|
|
160
|
-
<script defer src="./js/eos-
|
|
157
|
+
<script defer src="./js/eos-branding.js?v=49"></script>
|
|
158
|
+
<script defer src="./js/eos-security-ui.js?v=49"></script>
|
|
159
|
+
<script defer src="./js/eos-role-ui.js?v=49"></script>
|
|
160
|
+
<script defer src="./js/eos-assistant.js?v=49"></script>
|
|
161
|
+
<script defer src="./js/eos-objects-state-tools.js?v=49"></script>
|
|
161
162
|
</head>
|
|
162
163
|
<body>
|
|
163
164
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
window.NEXOWATT_EOS_UI_VERSION = '
|
|
4
|
+
window.NEXOWATT_EOS_UI_VERSION = 'v49-dp-write-performance-fix';
|
|
5
5
|
|
|
6
6
|
const BRAND = 'NexoWatt EOS';
|
|
7
7
|
const EOS_MEANING = 'Energy Operation System';
|
|
@@ -945,6 +945,8 @@
|
|
|
945
945
|
|
|
946
946
|
const desiredPermission = (profile, block, perm) => {
|
|
947
947
|
if (!perm) return null;
|
|
948
|
+
if (profile === 'enduser') profile = 'operator';
|
|
949
|
+
if (profile === 'installer') profile = 'service';
|
|
948
950
|
if (profile === 'admin') return true;
|
|
949
951
|
if (profile === 'viewer') return ['object', 'state', 'file'].includes(block) && ['list', 'read'].includes(perm);
|
|
950
952
|
if (profile === 'operator') {
|
|
@@ -981,13 +983,12 @@
|
|
|
981
983
|
const panel = document.createElement('section');
|
|
982
984
|
panel.className = 'eos-permission-presets';
|
|
983
985
|
panel.innerHTML = `
|
|
984
|
-
<div class="eos-permission-presets-title">
|
|
985
|
-
<div class="eos-permission-presets-text">Wähle ein Profil und passe danach einzelne Rechte an
|
|
986
|
+
<div class="eos-permission-presets-title">EOS Rollen-Schnellprofile</div>
|
|
987
|
+
<div class="eos-permission-presets-text">NexoWatt EOS arbeitet mit drei Rollen: Admin, Installateur und Endkunde. Wähle ein Profil und passe danach bei Bedarf einzelne Rechte an.</div>
|
|
986
988
|
<div class="eos-permission-presets-actions">
|
|
987
|
-
<button type="button" data-profile="
|
|
988
|
-
<button type="button" data-profile="
|
|
989
|
-
<button type="button" data-profile="
|
|
990
|
-
<button type="button" data-profile="admin">Vollzugriff</button>
|
|
989
|
+
<button type="button" data-profile="enduser">Endkunde</button>
|
|
990
|
+
<button type="button" data-profile="installer">Installateur</button>
|
|
991
|
+
<button type="button" data-profile="admin">Admin</button>
|
|
991
992
|
</div>
|
|
992
993
|
`;
|
|
993
994
|
panel.addEventListener('click', event => {
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
window.NEXOWATT_EOS_OBJECTS_STATE_TOOLS_VERSION = '
|
|
4
|
+
window.NEXOWATT_EOS_OBJECTS_STATE_TOOLS_VERSION = 'v49-dp-write-performance-fix';
|
|
5
5
|
|
|
6
6
|
const ACTIVE_CLASS = 'eos-objects-surface';
|
|
7
|
-
const safe = fn => { try { return fn(); } catch (
|
|
7
|
+
const safe = fn => { try { return fn(); } catch (_) { return undefined; } };
|
|
8
8
|
|
|
9
9
|
const isObjectsRoute = () => {
|
|
10
10
|
const hash = String(window.location.hash || '');
|
|
11
11
|
if (/#tab-objects/.test(hash)) return true;
|
|
12
|
-
|
|
13
|
-
if (/[?&]tab=objects/.test(url)) return true;
|
|
14
|
-
return false;
|
|
12
|
+
return /[?&]tab=objects/.test(String(window.location.href || ''));
|
|
15
13
|
};
|
|
16
14
|
|
|
17
15
|
const setSurfaceState = () => {
|
|
@@ -52,119 +50,127 @@
|
|
|
52
50
|
'[role="tooltip"]'
|
|
53
51
|
].join(',');
|
|
54
52
|
|
|
53
|
+
const valueCellSelector = [
|
|
54
|
+
'.eos-object-value-cell',
|
|
55
|
+
'[data-eos-object-value-cell]',
|
|
56
|
+
'[title*="Wert schreiben"]',
|
|
57
|
+
'[title*="Button sofort testen"]'
|
|
58
|
+
].join(',');
|
|
59
|
+
|
|
60
|
+
const collect = (base, selector) => {
|
|
61
|
+
const out = [];
|
|
62
|
+
if (!base) return out;
|
|
63
|
+
if (base.nodeType === Node.ELEMENT_NODE && base.matches?.(selector)) out.push(base);
|
|
64
|
+
if (base.querySelectorAll) out.push(...Array.from(base.querySelectorAll(selector)));
|
|
65
|
+
return out;
|
|
66
|
+
};
|
|
67
|
+
|
|
55
68
|
const releaseNativeControls = root => safe(() => {
|
|
56
69
|
if (!setSurfaceState()) return;
|
|
57
70
|
const base = root && root.nodeType ? root : document;
|
|
58
|
-
|
|
59
|
-
if (base.nodeType === Node.ELEMENT_NODE && base.matches?.(interactiveSelector)) nodes.push(base);
|
|
60
|
-
nodes.push(...Array.from(base.querySelectorAll?.(interactiveSelector) || []));
|
|
61
|
-
nodes.forEach(el => {
|
|
71
|
+
collect(base, `${interactiveSelector},${nativeLayerSelector}`).forEach(el => {
|
|
62
72
|
if (!el || el.closest?.('#eos-assist-root,.eos-assist-root')) return;
|
|
63
73
|
el.classList?.remove('eos-security-hidden-delete', 'eos-protected-delete-control', 'eos-disabled-by-security');
|
|
64
74
|
el.removeAttribute?.('data-eos-security-blocked');
|
|
65
75
|
el.removeAttribute?.('aria-disabled');
|
|
66
|
-
el.style.pointerEvents = 'auto';
|
|
67
|
-
if (el.style
|
|
68
|
-
if (el.style
|
|
69
|
-
});
|
|
70
|
-
const layers = [];
|
|
71
|
-
if (base.nodeType === Node.ELEMENT_NODE && base.matches?.(nativeLayerSelector)) layers.push(base);
|
|
72
|
-
layers.push(...Array.from(base.querySelectorAll?.(nativeLayerSelector) || []));
|
|
73
|
-
layers.forEach(el => {
|
|
74
|
-
if (!el) return;
|
|
75
|
-
el.style.pointerEvents = 'auto';
|
|
76
|
+
if (el.style?.pointerEvents === 'none') el.style.pointerEvents = 'auto';
|
|
77
|
+
if (el.style?.visibility === 'hidden') el.style.visibility = 'visible';
|
|
78
|
+
if (el.style?.display === 'none' && !el.classList?.contains('eos-native-logout-hidden')) el.style.display = '';
|
|
76
79
|
});
|
|
77
80
|
});
|
|
78
81
|
|
|
79
|
-
const formatAge = ts => {
|
|
80
|
-
const n = Number(ts);
|
|
81
|
-
if (!Number.isFinite(n) || n <= 0) return 'unbekannt';
|
|
82
|
-
try { return new Date(n).toLocaleString(); } catch { return String(n); }
|
|
83
|
-
};
|
|
84
|
-
|
|
85
82
|
const annotateValueCells = root => safe(() => {
|
|
86
83
|
if (!setSurfaceState()) return;
|
|
87
84
|
const base = root && root.nodeType ? root : document;
|
|
88
|
-
|
|
89
|
-
const valueCellSelector = '.eos-object-value-cell,[data-eos-object-value-cell],[title*="Wert schreiben"],[title*="Button sofort testen"]';
|
|
90
|
-
if (base.nodeType === Node.ELEMENT_NODE && base.matches?.(valueCellSelector)) cells.push(base);
|
|
91
|
-
cells.push(...Array.from(base.querySelectorAll?.(valueCellSelector) || []));
|
|
92
|
-
cells.forEach(cell => {
|
|
85
|
+
collect(base, valueCellSelector).forEach(cell => {
|
|
93
86
|
const id = cell.getAttribute('data-eos-object-value-cell') || cell.closest?.('[id]')?.getAttribute('id') || '';
|
|
94
|
-
const
|
|
95
|
-
const writable = cell.getAttribute('data-eos-object-writable') === '1' || /Wert schreiben|Button sofort testen/i.test(
|
|
87
|
+
const nativeTitle = cell.getAttribute('title') || '';
|
|
88
|
+
const writable = cell.getAttribute('data-eos-object-writable') === '1' || /Wert schreiben|Button sofort testen/i.test(nativeTitle);
|
|
96
89
|
const visibleValue = (cell.textContent || '').trim().replace(/\s+/g, ' ') || '(leer)';
|
|
90
|
+
const cache = `${id}|${writable ? 1 : 0}|${visibleValue}`;
|
|
91
|
+
|
|
97
92
|
cell.style.pointerEvents = 'auto';
|
|
98
93
|
cell.style.cursor = writable ? 'pointer' : 'default';
|
|
94
|
+
cell.classList.add('eos-object-value-cell');
|
|
99
95
|
cell.classList.toggle('eos-object-value-writable', writable);
|
|
100
96
|
cell.classList.toggle('eos-object-value-readonly', !writable);
|
|
101
|
-
|
|
102
|
-
cell.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
].filter(Boolean).join('\n'));
|
|
97
|
+
|
|
98
|
+
if (cell.getAttribute('data-eos-value-cache') !== cache) {
|
|
99
|
+
cell.setAttribute('data-eos-value-cache', cache);
|
|
100
|
+
cell.setAttribute('data-eos-current-visible-value', visibleValue);
|
|
101
|
+
const nextTitle = [
|
|
102
|
+
id ? `ID: ${id}` : null,
|
|
103
|
+
`Aktueller Wert: ${visibleValue}`,
|
|
104
|
+
`Status: ${writable ? 'beschreibbar' : 'nur lesbar'}`,
|
|
105
|
+
writable ? 'Klicken: Wert schreiben / Button sofort testen' : 'Nur lesbarer Wert'
|
|
106
|
+
].filter(Boolean).join('\n');
|
|
107
|
+
if (cell.getAttribute('title') !== nextTitle) cell.setAttribute('title', nextTitle);
|
|
108
|
+
}
|
|
114
109
|
});
|
|
115
110
|
});
|
|
116
111
|
|
|
117
|
-
const
|
|
112
|
+
const run = root => {
|
|
118
113
|
if (!setSurfaceState()) return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (cell.getAttribute('data-eos-object-writable') === '1') {
|
|
122
|
-
cell.classList.add('eos-object-value-hover');
|
|
123
|
-
}
|
|
114
|
+
releaseNativeControls(root);
|
|
115
|
+
annotateValueCells(root);
|
|
124
116
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
|
|
118
|
+
let scheduled = false;
|
|
119
|
+
const pendingRoots = new Set();
|
|
120
|
+
const schedule = root => {
|
|
121
|
+
if (!setSurfaceState()) return;
|
|
122
|
+
if (root) pendingRoots.add(root);
|
|
123
|
+
if (scheduled) return;
|
|
124
|
+
scheduled = true;
|
|
125
|
+
const execute = () => {
|
|
126
|
+
scheduled = false;
|
|
127
|
+
const roots = pendingRoots.size ? Array.from(pendingRoots) : [document];
|
|
128
|
+
pendingRoots.clear();
|
|
129
|
+
roots.slice(0, 60).forEach(run);
|
|
130
|
+
};
|
|
131
|
+
if ('requestIdleCallback' in window) window.requestIdleCallback(execute, { timeout: 500 });
|
|
132
|
+
else window.requestAnimationFrame(execute);
|
|
128
133
|
};
|
|
129
134
|
|
|
130
|
-
const
|
|
135
|
+
const handlePointerAssist = event => {
|
|
131
136
|
if (!setSurfaceState()) return;
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
const cell = event.target?.closest?.('.eos-object-value-cell');
|
|
138
|
+
if (cell?.getAttribute('data-eos-object-writable') === '1') cell.classList.add('eos-object-value-hover');
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handlePointerLeave = event => {
|
|
142
|
+
event.target?.closest?.('.eos-object-value-cell')?.classList.remove('eos-object-value-hover');
|
|
134
143
|
};
|
|
135
144
|
|
|
136
|
-
window.addEventListener('hashchange', () => setTimeout(() =>
|
|
137
|
-
window.addEventListener('popstate', () => setTimeout(() =>
|
|
145
|
+
window.addEventListener('hashchange', () => window.setTimeout(() => schedule(document), 30));
|
|
146
|
+
window.addEventListener('popstate', () => window.setTimeout(() => schedule(document), 30));
|
|
138
147
|
document.addEventListener('mouseover', handlePointerAssist, true);
|
|
139
148
|
document.addEventListener('mouseout', handlePointerLeave, true);
|
|
140
149
|
document.addEventListener('click', event => {
|
|
141
150
|
if (!setSurfaceState()) return;
|
|
142
|
-
const nativeInteractive = event.target?.closest?.(interactiveSelector
|
|
151
|
+
const nativeInteractive = event.target?.closest?.(`${interactiveSelector},${nativeLayerSelector},.eos-object-value-cell`);
|
|
143
152
|
if (nativeInteractive) {
|
|
144
|
-
//
|
|
153
|
+
// Keep native React/ObjectBrowser handlers untouched; only repair stale CSS/attributes after the click.
|
|
145
154
|
releaseNativeControls(nativeInteractive);
|
|
146
|
-
window.setTimeout(() =>
|
|
147
|
-
window.setTimeout(() => annotateValueCells(document), 120);
|
|
155
|
+
window.setTimeout(() => schedule(nativeInteractive), 80);
|
|
148
156
|
}
|
|
149
157
|
}, false);
|
|
150
158
|
|
|
151
159
|
const observer = new MutationObserver(records => {
|
|
152
160
|
if (!setSurfaceState()) return;
|
|
153
|
-
let roots = [];
|
|
154
161
|
for (const rec of records) {
|
|
155
162
|
rec.addedNodes?.forEach(node => {
|
|
156
|
-
if (node.nodeType === Node.ELEMENT_NODE)
|
|
163
|
+
if (node.nodeType === Node.ELEMENT_NODE) schedule(node);
|
|
157
164
|
});
|
|
158
165
|
}
|
|
159
|
-
if (!roots.length) roots = [document];
|
|
160
|
-
window.requestAnimationFrame?.(() => roots.forEach(run)) || roots.forEach(run);
|
|
161
166
|
});
|
|
162
167
|
|
|
163
168
|
const start = () => {
|
|
164
169
|
setSurfaceState();
|
|
165
|
-
|
|
170
|
+
schedule(document);
|
|
166
171
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
167
|
-
|
|
172
|
+
// Low-frequency safety pass only; v45 scanned the complete page every 2.5s and hurt large object lists.
|
|
173
|
+
window.setInterval(() => schedule(document), 15000);
|
|
168
174
|
};
|
|
169
175
|
|
|
170
176
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
window.NEXOWATT_EOS_ROLE_UI_VERSION = 'v49-dp-write-performance-fix';
|
|
5
|
+
|
|
6
|
+
const ASSET_BASE = (() => {
|
|
7
|
+
const script = document.currentScript?.src || document.querySelector('script[src*="eos-role-ui.js"]')?.src || window.location.href;
|
|
8
|
+
return new URL('../', script).href;
|
|
9
|
+
})();
|
|
10
|
+
|
|
11
|
+
const state = {
|
|
12
|
+
policy: null,
|
|
13
|
+
role: 'unknown',
|
|
14
|
+
scheduled: false,
|
|
15
|
+
observer: null,
|
|
16
|
+
fallbackTimer: null,
|
|
17
|
+
redirects: 0,
|
|
18
|
+
lastTarget: '',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const securityEndpointUrls = () => [
|
|
22
|
+
new URL('nexowatt/security/context', ASSET_BASE).href,
|
|
23
|
+
new URL('nexowatt/security/session', ASSET_BASE).href,
|
|
24
|
+
new URL('eos/security/status', ASSET_BASE).href,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const safe = fn => {
|
|
28
|
+
try { return fn(); } catch (_) { return undefined; }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const normalize = value => String(value || '')
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.normalize('NFD')
|
|
34
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
35
|
+
.replace(/ß/g, 'ss')
|
|
36
|
+
.replace(/[^a-z0-9#/_:.-]+/g, ' ')
|
|
37
|
+
.trim();
|
|
38
|
+
|
|
39
|
+
const stripTab = value => String(value || '').replace(/^#?\/?/, '').replace(/^tab-/, '');
|
|
40
|
+
|
|
41
|
+
const hashTab = () => {
|
|
42
|
+
const hash = decodeURIComponent(window.location.hash || '');
|
|
43
|
+
const match = hash.match(/#\/?(tab-[a-z0-9_-]+(?:-\d+)?)\b/i);
|
|
44
|
+
if (match) return match[1];
|
|
45
|
+
if (!hash || hash === '#' || hash === '#/' || hash === '#/tab-intro') return 'tab-intro';
|
|
46
|
+
return '';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const tabFromHref = href => {
|
|
50
|
+
const value = String(href || '');
|
|
51
|
+
const match = value.match(/#\/?(tab-[a-z0-9_-]+(?:-\d+)?)\b/i);
|
|
52
|
+
return match ? match[1] : '';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const textOf = el => {
|
|
56
|
+
if (!el) return '';
|
|
57
|
+
const attrs = ['href', 'title', 'aria-label', 'data-name', 'data-tab', 'data-id', 'id'];
|
|
58
|
+
const values = [el.textContent || ''];
|
|
59
|
+
attrs.forEach(attr => {
|
|
60
|
+
if (el.getAttribute && el.hasAttribute(attr)) values.push(el.getAttribute(attr) || '');
|
|
61
|
+
});
|
|
62
|
+
return values.join(' ');
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isLoginView = () => /(?:^|[?&])login(?:[=&]|$)/i.test(window.location.search || '') ||
|
|
66
|
+
!!document.querySelector('.login-form, #login, [data-testid="login"]');
|
|
67
|
+
|
|
68
|
+
const roleFromPolicy = policy => {
|
|
69
|
+
const rawRole = normalize(policy?.role || policy?.eosRole || policy?.accessRole || '');
|
|
70
|
+
if (policy?.isAdmin || policy?.isEosAdminGroup || policy?.isAdministrator || rawRole === 'admin' || rawRole === 'administrator') return 'admin';
|
|
71
|
+
if (policy?.isInstaller || /installateur|installer|service|techniker|technician|integrator|partner|wartung|maintenance/.test(rawRole)) return 'installer';
|
|
72
|
+
if (policy?.isEndUser || /endkunde|endkunden|kunde|kunden|customer|bediener|operator|viewer|user|nutzer/.test(rawRole)) return 'enduser';
|
|
73
|
+
|
|
74
|
+
const groupText = normalize([
|
|
75
|
+
...(Array.isArray(policy?.groups) ? policy.groups : []),
|
|
76
|
+
...(Array.isArray(policy?.groupNames) ? policy.groupNames : []),
|
|
77
|
+
].join(' '));
|
|
78
|
+
if (/system\.group\.administrator|\badministrator\b/.test(groupText)) return 'admin';
|
|
79
|
+
if (/installateur|installer|service|techniker|technician|integrator|partner|wartung|maintenance/.test(groupText)) return 'installer';
|
|
80
|
+
if (/endkunde|endkunden|kunde|kunden|customer|bediener|operator|viewer|nutzer/.test(groupText)) return 'enduser';
|
|
81
|
+
|
|
82
|
+
// Secure default: a normal, non-admin ioBroker user is an EOS end customer.
|
|
83
|
+
return 'enduser';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const setRoleClasses = role => safe(() => {
|
|
87
|
+
const root = document.documentElement;
|
|
88
|
+
root.classList.add('eos-role-loaded');
|
|
89
|
+
['admin', 'installer', 'installateur', 'enduser', 'unknown'].forEach(name => root.classList.remove(`eos-role-${name}`));
|
|
90
|
+
root.classList.add(`eos-role-${role}`);
|
|
91
|
+
if (role === 'installer') root.classList.add('eos-role-installateur');
|
|
92
|
+
if (role === 'admin') {
|
|
93
|
+
root.classList.add('eos-security-admin');
|
|
94
|
+
root.classList.remove('eos-security-nonadmin');
|
|
95
|
+
} else {
|
|
96
|
+
root.classList.add('eos-security-nonadmin');
|
|
97
|
+
root.classList.remove('eos-security-admin');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const endUserAllowedByText = value => {
|
|
102
|
+
const text = normalize(value);
|
|
103
|
+
if (!text) return false;
|
|
104
|
+
if (/(eos cockpit|nexowatt ui|nexowatt cockpit|kunden cockpit|kundenbereich|endkundenbereich|visualisierung|visualisation|visu|dashboard|energie cockpit|energy cockpit|bedienung|smart home|smart-home)/.test(text)) return true;
|
|
105
|
+
if (/\b(lovelace|jarvis|vis|iqontrol|material)\b/.test(text)) return true;
|
|
106
|
+
return false;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const endUserDeniedByText = value => {
|
|
110
|
+
const text = normalize(value);
|
|
111
|
+
return /(module|dienste|datenpunkte|objekte|struktur|systemlogs|logs|zugange|rechte|benutzer|users|gruppen|groups|system hosts|system-hosts|hosts|dateien|files|sicherung|backup|konsole|console|terminal|xterm|admin|adapter|instances?|instanzen|geraetesuche|discovery|systemschutz|security)/.test(text);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const isEndUserTab = (tab, label = '') => {
|
|
115
|
+
const clean = normalize(tab);
|
|
116
|
+
const text = `${clean} ${normalize(label)}`;
|
|
117
|
+
if (!clean || clean === 'tab-intro') return false;
|
|
118
|
+
if (/^tab-(nexowatt-ui|nexowatt-cockpit|eos-cockpit|eos-dashboard|kunden-cockpit|endkunden-cockpit)(?:-|$)/.test(clean)) return true;
|
|
119
|
+
if (/^tab-(lovelace|jarvis|vis|iqontrol|material)(?:-|$)/.test(clean)) return true;
|
|
120
|
+
if (endUserDeniedByText(text)) return false;
|
|
121
|
+
return endUserAllowedByText(text);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isInstallerDenied = (tab, label = '') => {
|
|
125
|
+
const text = `${normalize(tab)} ${normalize(label)}`;
|
|
126
|
+
return /(?:tab-users|tab-hosts|tab-xterm|tab-xtrem|tab-admin|tab-system|zugange\s*&?\s*rechte|zugange|rechte|benutzer|users|gruppen|system-hosts|system hosts|hosts|konsole|console|terminal|xterm|sicherung|backup|systemschutz|security|legacy admin)/.test(text);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const isRouteAllowed = (role, tab, label = '') => {
|
|
130
|
+
if (role === 'admin') return true;
|
|
131
|
+
if (role === 'installer') return !isInstallerDenied(tab, label);
|
|
132
|
+
if (role === 'enduser') return isEndUserTab(tab, label);
|
|
133
|
+
return false;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const navEntries = () => {
|
|
137
|
+
const anchors = Array.from(document.querySelectorAll('a[href*="#tab-"], a[href*="#/tab-"]'));
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
return anchors.map(anchor => {
|
|
140
|
+
const tab = tabFromHref(anchor.getAttribute('href') || anchor.href || '');
|
|
141
|
+
if (!tab || seen.has(anchor)) return null;
|
|
142
|
+
seen.add(anchor);
|
|
143
|
+
const root = anchor.closest('.MuiListItem-root, .MuiButtonBase-root, li, [role="button"], [class*="DrawerItem"], [class*="dragWrapper"]') || anchor;
|
|
144
|
+
return { anchor, root, tab, label: textOf(root) || textOf(anchor) };
|
|
145
|
+
}).filter(Boolean);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const firstAllowedTab = role => {
|
|
149
|
+
const entries = navEntries();
|
|
150
|
+
const hit = entries.find(entry => isRouteAllowed(role, entry.tab, entry.label));
|
|
151
|
+
if (hit) return hit.tab;
|
|
152
|
+
if (role === 'enduser') return 'tab-nexowatt-ui';
|
|
153
|
+
if (role === 'installer') return 'tab-instances';
|
|
154
|
+
return 'tab-intro';
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const applyMenuPolicy = () => safe(() => {
|
|
158
|
+
if (!state.policy || !state.role || state.role === 'unknown') return;
|
|
159
|
+
navEntries().forEach(entry => {
|
|
160
|
+
const hide = !isRouteAllowed(state.role, entry.tab, entry.label);
|
|
161
|
+
[entry.anchor, entry.root].forEach(el => {
|
|
162
|
+
if (!el) return;
|
|
163
|
+
el.classList.toggle('eos-role-hidden', hide);
|
|
164
|
+
if (hide) el.setAttribute('aria-hidden', 'true');
|
|
165
|
+
else el.removeAttribute('aria-hidden');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const setHashTab = tab => {
|
|
171
|
+
if (!tab) return;
|
|
172
|
+
if (state.lastTarget === tab && Date.now() - Number(sessionStorage.getItem('eosRoleLastRedirectAt') || 0) < 1200) return;
|
|
173
|
+
state.lastTarget = tab;
|
|
174
|
+
state.redirects += 1;
|
|
175
|
+
sessionStorage.setItem('eosRoleLastRedirectAt', String(Date.now()));
|
|
176
|
+
const next = `#${tab}`;
|
|
177
|
+
if (window.location.hash !== next) window.location.hash = next;
|
|
178
|
+
window.dispatchEvent(new CustomEvent('eos-role-navigate', { detail: { tab } }));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const redirectIfNeeded = () => safe(() => {
|
|
182
|
+
if (isLoginView()) return;
|
|
183
|
+
const role = state.role;
|
|
184
|
+
if (!role || role === 'unknown' || role === 'admin') return;
|
|
185
|
+
const tab = hashTab() || 'tab-intro';
|
|
186
|
+
if (isRouteAllowed(role, tab, '')) return;
|
|
187
|
+
const target = firstAllowedTab(role);
|
|
188
|
+
if (target && target !== tab && state.redirects < 6) setHashTab(target);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const appPaper = () => document.getElementById('app-paper') || document.querySelector('main, #root');
|
|
192
|
+
|
|
193
|
+
const appLooksBlank = () => {
|
|
194
|
+
const paper = appPaper();
|
|
195
|
+
if (!paper) return false;
|
|
196
|
+
const tab = hashTab();
|
|
197
|
+
const text = normalize(paper.textContent || '');
|
|
198
|
+
const interactive = paper.querySelectorAll('iframe, a, button, input, select, textarea, table, [role="grid"], .MuiDataGrid-root, .MuiTable-root').length;
|
|
199
|
+
if (state.role === 'enduser' && (!isEndUserTab(tab, text) || tab === 'tab-intro')) return true;
|
|
200
|
+
return text.length < 12 && interactive === 0;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const showLandingFallback = () => safe(() => {
|
|
204
|
+
if (isLoginView() || state.role !== 'enduser') return;
|
|
205
|
+
if (!appLooksBlank()) {
|
|
206
|
+
document.getElementById('eos-role-landing')?.remove();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const paper = appPaper();
|
|
210
|
+
if (!paper) return;
|
|
211
|
+
let landing = document.getElementById('eos-role-landing');
|
|
212
|
+
if (!landing) {
|
|
213
|
+
landing = document.createElement('section');
|
|
214
|
+
landing.id = 'eos-role-landing';
|
|
215
|
+
landing.className = 'eos-role-landing';
|
|
216
|
+
landing.innerHTML = `
|
|
217
|
+
<div class="eos-role-landing-card">
|
|
218
|
+
<div class="eos-role-landing-eyebrow">NexoWatt EOS</div>
|
|
219
|
+
<h1>Endkundenbereich</h1>
|
|
220
|
+
<p>Dieser Benutzer ist als Endkunde angemeldet. Die Administrations-Startseite ist für diese Rolle gesperrt; EOS öffnet stattdessen das Kunden-Cockpit.</p>
|
|
221
|
+
<button type="button" class="eos-role-open-cockpit">EOS Cockpit öffnen</button>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
landing.querySelector('.eos-role-open-cockpit')?.addEventListener('click', event => {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
setHashTab(firstAllowedTab('enduser') || 'tab-nexowatt-ui');
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (!paper.contains(landing)) {
|
|
230
|
+
paper.appendChild(landing);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const scheduleFallbackCheck = () => {
|
|
235
|
+
clearTimeout(state.fallbackTimer);
|
|
236
|
+
state.fallbackTimer = setTimeout(() => {
|
|
237
|
+
showLandingFallback();
|
|
238
|
+
}, 1000);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const apply = () => {
|
|
242
|
+
if (!state.policy) return;
|
|
243
|
+
setRoleClasses(state.role);
|
|
244
|
+
applyMenuPolicy();
|
|
245
|
+
redirectIfNeeded();
|
|
246
|
+
scheduleFallbackCheck();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const scheduleApply = () => {
|
|
250
|
+
if (state.scheduled) return;
|
|
251
|
+
state.scheduled = true;
|
|
252
|
+
requestAnimationFrame(() => {
|
|
253
|
+
state.scheduled = false;
|
|
254
|
+
apply();
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const fetchPolicy = async () => {
|
|
259
|
+
for (const url of securityEndpointUrls()) {
|
|
260
|
+
try {
|
|
261
|
+
const response = await fetch(url, { credentials: 'same-origin', cache: 'no-store' });
|
|
262
|
+
if (!response.ok) continue;
|
|
263
|
+
const json = await response.json();
|
|
264
|
+
const role = roleFromPolicy(json);
|
|
265
|
+
state.policy = { ...json, role };
|
|
266
|
+
state.role = role;
|
|
267
|
+
window.NEXOWATT_EOS_ROLE_POLICY = state.policy;
|
|
268
|
+
scheduleApply();
|
|
269
|
+
return;
|
|
270
|
+
} catch (_) {
|
|
271
|
+
// try next endpoint
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
state.policy = { role: 'enduser', isEndUser: true, groups: [] };
|
|
275
|
+
state.role = 'enduser';
|
|
276
|
+
window.NEXOWATT_EOS_ROLE_POLICY = state.policy;
|
|
277
|
+
scheduleApply();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const startObserver = () => safe(() => {
|
|
281
|
+
if (state.observer || !document.documentElement) return;
|
|
282
|
+
state.observer = new MutationObserver(scheduleApply);
|
|
283
|
+
state.observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['href', 'class', 'aria-label'] });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const start = () => {
|
|
287
|
+
fetchPolicy();
|
|
288
|
+
startObserver();
|
|
289
|
+
window.addEventListener('hashchange', () => {
|
|
290
|
+
state.redirects = 0;
|
|
291
|
+
scheduleApply();
|
|
292
|
+
});
|
|
293
|
+
window.addEventListener('storage', scheduleApply);
|
|
294
|
+
setTimeout(scheduleApply, 400);
|
|
295
|
+
setTimeout(scheduleApply, 1500);
|
|
296
|
+
setTimeout(scheduleApply, 3500);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (document.readyState === 'loading') {
|
|
300
|
+
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
301
|
+
} else {
|
|
302
|
+
start();
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const VERSION = '
|
|
4
|
+
const VERSION = 'v49-dp-write-performance-fix';
|
|
5
5
|
const LEGACY_ADMIN = 'admin';
|
|
6
6
|
const LEGACY_ADMIN_INSTANCE = 'admin.0';
|
|
7
7
|
const CORE_PROTECTED_ADAPTERS = ['admin', 'eos-admin', 'backitup', 'nexowatt-devices', 'nexowatt-device', 'nexowatt-dev', 'nexowatt-ui'];
|