iobroker.eos-admin 7.9.96 → 7.9.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/INSTALL_TEST_V7.9.98.md +39 -0
  2. package/MERGE_UPDATE_README_V7.9.98.md +29 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +20 -18
  4. package/PUBLISH_STABLE_V7.9.98.md +33 -0
  5. package/README.md +16 -6
  6. package/README_STABILITY_V7.9.98.md +22 -0
  7. package/RELEASE_ACCEPTANCE_V7.9.98.md +48 -0
  8. package/RELEASE_NOTES_V7.9.98.md +32 -0
  9. package/adminWww/assets/Intro-DkwRiz1n-v84.js +1 -1
  10. package/adminWww/assets/bootstrap-COulQZax-v84.js +3 -3
  11. package/adminWww/assets/index-D2ymscJA-v84.js +1 -1
  12. package/adminWww/css/eos-ems-overview.css +21 -0
  13. package/adminWww/css/nexowatt-native-shell.css +174 -40
  14. package/adminWww/index.html +15 -15
  15. package/adminWww/js/eos-account-management.js +26 -13
  16. package/adminWww/js/eos-assistant.js +1 -1
  17. package/adminWww/js/eos-ems-overview.js +136 -39
  18. package/adminWww/js/eos-role-bootstrap.js +7 -7
  19. package/adminWww/js/eos-role-ui.js +2 -2
  20. package/adminWww/js/nexowatt-native-shell.js +9 -2
  21. package/adminWww/static/js/nexowatt-stable-login.js +28 -62
  22. package/adminWww/static/js/nexowatt-stable-v98.js +33 -0
  23. package/build/lib/web.js +112 -79
  24. package/build/main.js +1 -1
  25. package/io-package.json +15 -5
  26. package/package.json +14 -12
  27. package/tools/nexowatt-account-management-selftest.cjs +16 -57
  28. package/tools/nexowatt-account-password-flow-selftest.cjs +186 -0
  29. package/tools/nexowatt-clean-legacy-runtime.cjs +33 -10
  30. package/tools/nexowatt-login-layout-selftest.cjs +12 -81
  31. package/tools/nexowatt-merge-update-selftest.cjs +3 -1
  32. package/tools/nexowatt-modern-ui-selftest.cjs +2 -2
  33. package/tools/nexowatt-password-write-selftest.cjs +48 -0
  34. package/tools/nexowatt-runtime-cleanup-selftest.cjs +9 -1
  35. package/tools/nexowatt-stable-v98-selftest.cjs +14 -0
  36. package/tools/nexowatt-sync-release-version.cjs +42 -1
  37. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +11 -11
  38. package/tools/nexowatt-validate-package.cjs +20 -11
  39. package/tools/nexowatt-version-sync-selftest.cjs +18 -1
  40. package/INSTALL_TEST_V7.9.96.md +0 -11
  41. package/MERGE_UPDATE_README_V7.9.96.md +0 -5
  42. package/PUBLISH_STABLE_V7.9.96.md +0 -10
  43. package/README_STABILITY_V7.9.96.md +0 -11
  44. package/RELEASE_ACCEPTANCE_V7.9.96.md +0 -10
  45. package/RELEASE_NOTES_V7.9.96.md +0 -12
  46. package/adminWww/static/js/nexowatt-stable-v96.js +0 -82
  47. package/tools/nexowatt-stable-v96-selftest.cjs +0 -13
@@ -1,50 +1,72 @@
1
1
  (() => {
2
2
  'use strict';
3
- const VERSION = 'v96-nexowatt-ems-overview-runtime';
3
+ const VERSION = 'v98-nexowatt-ems-overview-runtime';
4
4
  const CONTRACT = 'nexowatt-ems-overview-v1';
5
5
  const ROOT_ID = 'eos-ems-overview-runtime';
6
6
  const POLL_MS = 5000;
7
+ const RETRY_MS = 650;
8
+ const CONNECTION_WAIT_MS = 12000;
7
9
  const STALE_MS = 20000;
10
+ const REQUIRED_UI_VERSION = '0.8.198';
8
11
  const COLORS = { ok: '#01bc69', info: '#48b9ff', warning: '#ffbd59', error: '#ff5f72' };
9
12
  let timer = 0;
13
+ let retryTimer = 0;
10
14
  let inFlight = false;
11
15
  let destroyed = false;
16
+ let socketCache = null;
17
+ let startedAt = Date.now();
12
18
 
13
19
  const esc = value => String(value ?? '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[character]));
14
20
  const text = (value, fallback = '') => String(value ?? fallback).replace(/\s+/g, ' ').trim() || fallback;
15
21
  const num = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
16
- const parse = (value, fallback) => {
17
- if (value && typeof value === 'object') return value;
18
- try { return JSON.parse(String(value ?? '')); } catch (_) { return fallback; }
19
- };
20
- const severity = value => {
21
- const normalized = String(value || '').toLowerCase();
22
- if (normalized === 'error') return 'error';
23
- if (normalized === 'warning' || normalized === 'warn') return 'warning';
24
- if (normalized === 'ok') return 'ok';
25
- return 'info';
26
- };
22
+ const parse = (value, fallback) => { if (value && typeof value === 'object') return value; try { return JSON.parse(String(value ?? '')); } catch (_) { return fallback; } };
23
+ const severity = value => { const normalized = String(value || '').toLowerCase(); if (normalized === 'error') return 'error'; if (normalized === 'warning' || normalized === 'warn') return 'warning'; if (normalized === 'ok') return 'ok'; return 'info'; };
27
24
  const currentTab = () => String(location.hash || '').match(/tab-[a-z0-9_-]+/i)?.[0]?.toLowerCase() || 'tab-intro';
28
25
  const currentRole = () => String(window.NEXOWATT_EOS_ACCESS_ROLE || 'admin').toLowerCase();
29
26
  const technical = () => ['admin', 'service', 'installer', 'installateur'].includes(currentRole());
30
- const formatPower = value => {
31
- const watts = num(value); const absolute = Math.abs(watts); const sign = watts < 0 ? '−' : '';
32
- return absolute >= 1000 ? `${sign}${(absolute / 1000).toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 2 })} kW` : `${sign}${Math.round(absolute).toLocaleString('de-DE')} W`;
33
- };
34
- const formatAge = timestamp => {
35
- const value = num(timestamp); if (!value) return 'noch kein Regeltick';
36
- const seconds = Math.max(0, Math.round((Date.now() - value) / 1000));
37
- if (seconds < 2) return 'gerade eben';
38
- if (seconds < 60) return `vor ${seconds} s`;
39
- return `vor ${Math.round(seconds / 60)} min`;
40
- };
41
- const formatClock = timestamp => {
42
- const value = num(timestamp); if (!value) return '—';
43
- try { return new Date(value).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch (_) { return '—'; }
44
- };
45
- const getSocket = () => window.NEXOWATT_EOS_ADMIN_SOCKET || window.NEXOWATT_EOS_SOCKET || null;
27
+ const formatPower = value => { const watts = num(value); const absolute = Math.abs(watts); const sign = watts < 0 ? '−' : ''; return absolute >= 1000 ? `${sign}${(absolute / 1000).toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 2 })} kW` : `${sign}${Math.round(absolute).toLocaleString('de-DE')} W`; };
28
+ const formatAge = timestamp => { const value = num(timestamp); if (!value) return 'noch kein Regeltick'; const seconds = Math.max(0, Math.round((Date.now() - value) / 1000)); if (seconds < 2) return 'gerade eben'; if (seconds < 60) return `vor ${seconds} s`; return `vor ${Math.round(seconds / 60)} min`; };
29
+ const formatClock = timestamp => { const value = num(timestamp); if (!value) return '—'; try { return new Date(value).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch (_) { return '—'; } };
46
30
  const stateVal = (states, id) => states?.[id]?.val;
47
31
 
32
+ const isAdminSocket = candidate => Boolean(candidate && typeof candidate.getForeignStates === 'function');
33
+ const rememberSocket = candidate => {
34
+ if (!isAdminSocket(candidate)) return null;
35
+ socketCache = candidate;
36
+ try { window.NEXOWATT_EOS_ADMIN_SOCKET = candidate; window.NEXOWATT_EOS_SOCKET = candidate; } catch (_) { /* best effort */ }
37
+ return candidate;
38
+ };
39
+ const socketFromFrames = () => {
40
+ for (const frame of [window, window.parent, window.top]) {
41
+ try {
42
+ const candidate = frame?.NEXOWATT_EOS_ADMIN_SOCKET || frame?.NEXOWATT_EOS_SOCKET;
43
+ if (isAdminSocket(candidate)) return rememberSocket(candidate);
44
+ } catch (_) { /* cross-origin frame */ }
45
+ }
46
+ return null;
47
+ };
48
+ const socketFromReact = () => {
49
+ const roots = [document.getElementById('app-paper'), document.getElementById('root')].filter(Boolean);
50
+ const seen = new Set();
51
+ const inspect = value => {
52
+ if (!value || (typeof value !== 'object' && typeof value !== 'function') || seen.has(value)) return null;
53
+ seen.add(value);
54
+ if (isAdminSocket(value)) return rememberSocket(value);
55
+ for (const key of ['socket', 'props', 'memoizedProps', 'memoizedState', 'stateNode', 'return', 'child', 'sibling']) {
56
+ try { const found = inspect(value[key]); if (found) return found; } catch (_) { /* ignore inaccessible property */ }
57
+ }
58
+ return null;
59
+ };
60
+ for (const root of roots) {
61
+ for (const key of Object.getOwnPropertyNames(root)) {
62
+ if (!key.startsWith('__react')) continue;
63
+ try { const found = inspect(root[key]); if (found) return found; } catch (_) { /* ignore */ }
64
+ }
65
+ }
66
+ return null;
67
+ };
68
+ const getSocket = () => isAdminSocket(socketCache) ? socketCache : (socketFromFrames() || socketFromReact());
69
+
48
70
  const normalizeInstances = (states, aliveStates) => {
49
71
  const prefixes = new Set();
50
72
  Object.keys(states || {}).forEach(id => { const match = id.match(/^(nexowatt-ui\.\d+)\.info\.adminOverview\./); if (match) prefixes.add(match[1]); });
@@ -68,18 +90,53 @@
68
90
  if (!root) {
69
91
  root = document.createElement('section');
70
92
  root.id = ROOT_ID;
71
- root.className = 'eos-ems-overview-card';
93
+ root.className = 'eos-ems-overview-card is-loading';
72
94
  root.dataset.nexowattContract = CONTRACT;
73
95
  }
74
96
  const hero = document.getElementById('eos-native-overview-hero');
75
97
  if (hero?.parentElement === paper) paper.insertBefore(root, hero.nextSibling); else paper.insertBefore(root, paper.firstChild || null);
76
98
  return root;
77
99
  };
78
-
79
- const unavailable = message => {
100
+ const setMode = (root, mode) => { root.classList.remove('is-loading', 'is-unavailable', 'is-ready'); root.classList.add(`is-${mode}`); };
101
+ const loading = message => {
102
+ const root = ensureRoot(); if (!root) return;
103
+ setMode(root, 'loading');
104
+ root.style.setProperty('--eos-ems-status-color', COLORS.info);
105
+ root.innerHTML = `<header class="eos-ems-overview-header"><div><span class="eos-ems-overview-eyebrow">NexoWatt EMS</span><h2>Live-Diagnose wird verbunden</h2><p>${esc(message)}</p></div><div class="eos-ems-overview-state"><span class="eos-ems-overview-state-dot eos-ems-overview-state-dot--pulse"></span><strong>Verbindung</strong><small>automatischer Neuversuch</small></div></header>`;
106
+ };
107
+ const unavailable = (message, detail = '') => {
80
108
  const root = ensureRoot(); if (!root) return;
109
+ setMode(root, 'unavailable');
81
110
  root.style.setProperty('--eos-ems-status-color', COLORS.info);
82
- root.innerHTML = `<header class="eos-ems-overview-header"><div><span class="eos-ems-overview-eyebrow">NexoWatt EMS</span><h2>Live-Diagnose</h2><p>${esc(message)}</p></div><div class="eos-ems-overview-state"><span class="eos-ems-overview-state-dot"></span><strong>Nicht verfügbar</strong><small>read-only</small></div></header><footer class="eos-ems-overview-footer"><span>Die Kachel liest ausschließlich info.adminOverview.*.</span><span>Keine EMS-Regelung wurde verändert.</span></footer>`;
111
+ root.innerHTML = `<header class="eos-ems-overview-header"><div><span class="eos-ems-overview-eyebrow">NexoWatt EMS</span><h2>Live-Diagnose nicht verfügbar</h2><p>${esc(message)}</p>${detail ? `<small class="eos-ems-overview-help">${esc(detail)}</small>` : ''}</div><div class="eos-ems-overview-state"><span class="eos-ems-overview-state-dot"></span><strong>Nicht verfügbar</strong><small>read-only</small></div></header><footer class="eos-ems-overview-footer"><span>Die Kachel liest ausschließlich info.adminOverview.*.</span><span>Keine EMS-Regelung wurde verändert.</span></footer>`;
112
+ };
113
+ const semverAtLeast = (value, minimum) => {
114
+ const parseVersion = input => String(input || '').split('-')[0].split('.').map(part => Number.parseInt(part, 10) || 0);
115
+ const current = parseVersion(value); const required = parseVersion(minimum);
116
+ for (let index = 0; index < Math.max(current.length, required.length); index++) {
117
+ const left = current[index] || 0; const right = required[index] || 0;
118
+ if (left !== right) return left > right;
119
+ }
120
+ return true;
121
+ };
122
+ const readUiAdapterInfo = async socket => {
123
+ try {
124
+ if (typeof socket.getAdapterInstances === 'function') {
125
+ const rows = await socket.getAdapterInstances('nexowatt-ui', true);
126
+ const instances = Array.isArray(rows) ? rows : [];
127
+ const selected = instances.find(row => row?.common?.enabled) || instances[0];
128
+ if (selected) return { installed: true, enabled: selected.common?.enabled !== false, version: text(selected.common?.version, ''), id: text(selected._id, '') };
129
+ }
130
+ } catch (_) { /* next fallback */ }
131
+ try {
132
+ if (typeof socket.getObjectViewSystem === 'function') {
133
+ const rows = await socket.getObjectViewSystem('instance', 'system.adapter.nexowatt-ui.', 'system.adapter.nexowatt-ui.香');
134
+ const values = Array.isArray(rows) ? rows : Object.values(rows || {});
135
+ const selected = values.find(row => row?.common?.enabled) || values[0];
136
+ if (selected) return { installed: true, enabled: selected.common?.enabled !== false, version: text(selected.common?.version, ''), id: text(selected._id, '') };
137
+ }
138
+ } catch (_) { /* permission or compatibility */ }
139
+ return { installed: false, version: '', enabled: false, id: '' };
83
140
  };
84
141
 
85
142
  const fallbackDecisions = summary => {
@@ -91,9 +148,10 @@
91
148
 
92
149
  const render = data => {
93
150
  const root = ensureRoot(); if (!root) return;
151
+ setMode(root, 'ready');
94
152
  const summary = data.summary || {}; const budget = summary.budget || {}; const charging = summary.charging || {}; const storage = summary.storage || {}; const ems = summary.ems || {}; const para14a = summary.para14a || {}; const tariff = summary.tariff || {}; const forecast = summary.forecast || {}; const peak = summary.peakShaving || {};
95
153
  const stale = data.alive === false || !data.updatedAt || Date.now() - data.updatedAt > STALE_MS;
96
- const status = stale ? 'error' : severity(summary.status); const statusColor = COLORS[status]; root.style.setProperty('--eos-ems-status-color', statusColor);
154
+ const status = stale ? 'error' : severity(summary.status); root.style.setProperty('--eos-ems-status-color', COLORS[status]);
97
155
  const totalBudget = Math.max(0, num(budget.totalW)); const remaining = Math.max(0, num(budget.remainingW)); const percent = totalBudget > 0 ? Math.max(0, Math.min(100, remaining / totalBudget * 100)) : 0;
98
156
  const isTechnical = technical(); const decisions = (Array.isArray(summary.currentDecisions) ? summary.currentDecisions : fallbackDecisions(summary)).slice(0, isTechnical ? 5 : 2); const events = isTechnical ? data.events.slice(0, 6) : [];
99
157
  const tags = [];
@@ -110,17 +168,48 @@
110
168
  root.innerHTML = `<header class="eos-ems-overview-header"><div><span class="eos-ems-overview-eyebrow">NexoWatt EMS · Live-Diagnose</span><h2>${esc(stale ? 'EMS-Diagnose ist nicht aktuell' : text(summary.headline, 'EMS arbeitet normal'))}</h2><p>${esc(stale ? 'Adapter oder Diagnosewerte sind älter als 20 Sekunden. Die Werte werden als veraltet markiert.' : text(summary.reason, 'EMS arbeitet innerhalb aller aktiven Grenzen.'))}</p></div><div class="eos-ems-overview-state"><span class="eos-ems-overview-state-dot"></span><strong>${stale ? 'Offline / veraltet' : ({ok:'Normal',info:'Information',warning:'Begrenzt',error:'Störung'}[status])}</strong><small>${esc(data.instance)} · ${esc(formatAge(data.updatedAt))}</small></div></header><div class="eos-ems-overview-metrics">${metrics}</div>${totalBudget > 0 ? `<div class="eos-ems-overview-budget" title="${percent.toFixed(1)} % Restbudget"><span style="width:${percent}%"></span></div>` : ''}${tags.length ? `<div class="eos-ems-overview-tags">${tags.map(tag => `<span class="${tag.warning ? 'is-warning' : ''}">${esc(tag.label)}</span>`).join('')}</div>` : ''}<div class="eos-ems-overview-columns"><div class="eos-ems-overview-panel"><h3>Aktuelle Regelentscheidungen</h3>${decisionsHtml}</div><div class="eos-ems-overview-panel"><h3>${isTechnical ? 'Letzte EMS-Ereignisse' : 'Systemerklärung'}</h3>${eventsHtml}</div></div><footer class="eos-ems-overview-footer"><span>Diagnose ist rein lesend. Keine Kachel besitzt Schreibhoheit.</span><a href="${esc(detailsUrl)}" target="_blank" rel="noreferrer">Vollständige EMS-Diagnose öffnen ↗</a></footer>`;
111
169
  };
112
170
 
171
+ const scheduleFastRetry = () => {
172
+ if (destroyed || retryTimer) return;
173
+ retryTimer = window.setTimeout(() => { retryTimer = 0; void load(); }, RETRY_MS);
174
+ };
113
175
  const load = async () => {
114
176
  if (destroyed || inFlight || document.visibilityState === 'hidden' || currentTab() !== 'tab-intro') return;
115
177
  const socket = getSocket();
116
- if (!socket?.getForeignStates) { unavailable('Die Admin-Verbindung wird noch aufgebaut.'); return; }
178
+ if (!socket) {
179
+ if (Date.now() - startedAt < CONNECTION_WAIT_MS) {
180
+ loading('Die sichere Admin-Verbindung wird hergestellt. Die Kachel aktualisiert sich automatisch.');
181
+ scheduleFastRetry();
182
+ } else {
183
+ unavailable('Die Admin-Verbindung konnte nicht übernommen werden.', 'Bitte die Seite einmal mit Strg + F5 neu laden. Die übrige EOS-Oberfläche bleibt unverändert nutzbar.');
184
+ }
185
+ return;
186
+ }
117
187
  inFlight = true;
118
188
  try {
119
- const [states, alive] = await Promise.all([socket.getForeignStates('nexowatt-ui.*.info.adminOverview.*'), socket.getForeignStates('system.adapter.nexowatt-ui.*.alive')]);
189
+ const [states, alive] = await Promise.all([
190
+ socket.getForeignStates('nexowatt-ui.*.info.adminOverview.*'),
191
+ socket.getForeignStates('system.adapter.nexowatt-ui.*.alive'),
192
+ ]);
120
193
  const instances = normalizeInstances(states || {}, alive || {});
121
- if (instances.length) render(instances[0]); else unavailable('NexoWatt UI ist nicht installiert, offline oder der Diagnosevertrag info.adminOverview.* fehlt.');
122
- } catch (error) { unavailable(`EMS-Diagnose konnte nicht gelesen werden: ${text(error?.message || error, 'unbekannter Fehler')}`); }
123
- finally { inFlight = false; }
194
+ if (instances.length) {
195
+ render(instances[0]);
196
+ } else {
197
+ const info = await readUiAdapterInfo(socket);
198
+ if (!info.installed) {
199
+ unavailable('NexoWatt UI ist nicht installiert oder für dieses Benutzerkonto nicht sichtbar.', `Für die vollständige Kachel wird iobroker.nexowatt-ui ab ${REQUIRED_UI_VERSION} benötigt.`);
200
+ } else if (info.version && !semverAtLeast(info.version, REQUIRED_UI_VERSION)) {
201
+ unavailable(`NexoWatt UI ${info.version} ist installiert, enthält den Diagnosevertrag aber noch nicht.`, `Bitte NexoWatt UI mindestens auf ${REQUIRED_UI_VERSION} aktualisieren und die Instanz danach neu starten.`);
202
+ } else if (info.enabled === false) {
203
+ unavailable(`NexoWatt UI ${info.version || ''} ist installiert, aber deaktiviert.`, 'Bitte die UI-Instanz starten. Danach aktualisiert sich die Kachel automatisch.');
204
+ } else {
205
+ unavailable(`NexoWatt UI ${info.version || ''} ist aktiv, veröffentlicht aber noch keine info.adminOverview.*-Werte.`, 'Bitte die NexoWatt-UI-Instanz einmal neu starten. Bleibt der Channel aus, ist der AdminOverviewPublisher der installierten UI-Version zu prüfen.');
206
+ }
207
+ }
208
+ } catch (error) {
209
+ const message = text(error?.message || error, 'unbekannter Fehler');
210
+ const permission = /permission|access|not allowed|denied/i.test(message);
211
+ unavailable(permission ? 'Dieses Benutzerkonto darf die EMS-Diagnosewerte derzeit nicht lesen.' : `EMS-Diagnose konnte nicht gelesen werden: ${message}`, permission ? 'Die Datenpunkte unter nexowatt-ui.*.info.adminOverview.* benötigen Leserechte für die jeweilige EOS-Rolle.' : 'Die Kachel ist rein lesend; es wurde keine EMS-Regelung verändert.');
212
+ } finally { inFlight = false; }
124
213
  };
125
214
 
126
215
  const apply = () => {
@@ -129,12 +218,20 @@
129
218
  if (currentTab() !== 'tab-intro') { document.getElementById(ROOT_ID)?.remove(); return; }
130
219
  ensureRoot(); void load();
131
220
  };
221
+ const onSocketReady = event => {
222
+ rememberSocket(event?.detail?.socket || window.NEXOWATT_EOS_ADMIN_SOCKET || window.NEXOWATT_EOS_SOCKET);
223
+ startedAt = Date.now();
224
+ if (retryTimer) { clearTimeout(retryTimer); retryTimer = 0; }
225
+ void load();
226
+ };
132
227
  const start = () => {
228
+ startedAt = Date.now();
133
229
  apply();
134
230
  timer = window.setInterval(apply, POLL_MS);
135
231
  window.addEventListener('hashchange', apply);
232
+ window.addEventListener('nexowatt-eos-admin-socket-ready', onSocketReady);
136
233
  document.addEventListener('visibilitychange', apply);
137
234
  };
138
235
  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, { once: true }) : start();
139
- window.NEXOWATT_EOS_EMS_OVERVIEW = Object.freeze({ version: VERSION, contract: CONTRACT, refresh: apply, destroy() { destroyed = true; if (timer) clearInterval(timer); document.getElementById(ROOT_ID)?.remove(); } });
236
+ window.NEXOWATT_EOS_EMS_OVERVIEW = Object.freeze({ version: VERSION, contract: CONTRACT, refresh: apply, destroy() { destroyed = true; if (timer) clearInterval(timer); if (retryTimer) clearTimeout(retryTimer); window.removeEventListener('nexowatt-eos-admin-socket-ready', onSocketReady); document.getElementById(ROOT_ID)?.remove(); } });
140
237
  })();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v96-first-login-password-native-overview';
4
+ const VERSION = 'v98-first-login-password-native-overview';
5
5
  const script = document.currentScript || document.querySelector('script[src*="eos-role-bootstrap.js"]');
6
6
  const entry = script?.dataset?.eosEntry || '';
7
7
  let launched = false;
@@ -192,13 +192,13 @@
192
192
  submit.disabled = true; submit.textContent = t.saving; setStatus('', '');
193
193
  try {
194
194
  const response = await fetch(endpoint, {
195
- method: 'POST', credentials: 'same-origin', cache: 'no-store',
195
+ method: 'POST', credentials: 'include', cache: 'no-store',
196
196
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-NexoWatt-EOS-First-Login': '1' },
197
197
  body: JSON.stringify({ password: password.value, passwordRepeat: repeat.value }),
198
198
  });
199
199
  const data = await response.json().catch(() => ({}));
200
200
  if (!response.ok || data.error) {
201
- const messages = { passwordRequired: t.required, passwordMismatch: t.mismatch, passwordLength: t.length(minLength), passwordComplexity: t.complexity, passwordTooEasy: t.easy };
201
+ const messages = { passwordRequired: t.required, passwordMismatch: t.mismatch, passwordLength: t.length(minLength), passwordComplexity: t.complexity, passwordTooEasy: t.easy, passwordSetupNotAllowed: t.generic, invalidRequestOrigin: t.generic, passwordApiUnavailable: t.generic, passwordWriteFailed: t.generic, passwordVerificationFailed: t.generic, userObjectUnavailableAfterPasswordChange: t.generic };
202
202
  throw new Error(messages[data.error] || t.generic);
203
203
  }
204
204
  setStatus(t.success, 'success');
@@ -265,7 +265,7 @@
265
265
  submit.disabled = true; submit.textContent = t.saving; setStatus('', '');
266
266
  try {
267
267
  const response = await fetch(endpoint, {
268
- method: 'POST', credentials: 'same-origin', cache: 'no-store',
268
+ method: 'POST', credentials: 'include', cache: 'no-store',
269
269
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-NexoWatt-EOS-Passwordless-Password': '1' },
270
270
  body: JSON.stringify({ password: password.value, passwordRepeat: repeat.value }),
271
271
  });
@@ -473,7 +473,7 @@
473
473
  setStatus(card, t.claimChecking, '');
474
474
  try {
475
475
  const response = await fetch(new URL('nexowatt/account/passwordless-claim', base).href, {
476
- method: 'POST', credentials: 'same-origin', cache: 'no-store',
476
+ method: 'POST', credentials: 'include', cache: 'no-store',
477
477
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-NexoWatt-EOS-Passwordless-Claim': '1' },
478
478
  body: JSON.stringify({ user: account }),
479
479
  });
@@ -500,7 +500,7 @@
500
500
  const { username, password, card, submit } = loginElements();
501
501
  if (!username || !password || !card) return;
502
502
  card.classList.add('eos-login-card-modern');
503
- // v96 compact normal-login first activation: do not add role buttons or a second panel.
503
+ // v98 compact normal-login first activation: do not add role buttons or a second panel.
504
504
  // The account is entered in the existing Loginname field; a blank password is accepted only
505
505
  // after the backend confirms that this exact managed account is eligible.
506
506
  card.querySelectorAll('.eos-login-role-selector').forEach(element => element.remove());
@@ -634,7 +634,7 @@
634
634
  const timeout = window.setTimeout(() => controller.abort(), 8000);
635
635
 
636
636
  window.NEXOWATT_EOS_BOOTSTRAP_READY = fetch(contextUrl, {
637
- credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' }, signal: controller.signal,
637
+ credentials: 'include', cache: 'no-store', headers: { Accept: 'application/json' }, signal: controller.signal,
638
638
  })
639
639
  .then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); })
640
640
  .then(policy => {
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v96-native-overview-ems-live-reserve-filter';
4
+ const VERSION = 'v98-native-overview-ems-live-reserve-filter';
5
5
  window.NEXOWATT_EOS_ROLE_UI_VERSION = VERSION;
6
6
 
7
7
  const state = {
@@ -278,7 +278,7 @@
278
278
  });
279
279
  };
280
280
  const ensureSafeOverview = () => safe(() => {
281
- // 7.9.96: never cover the real Admin Intro with a second tile surface.
281
+ // 7.9.97: never cover the real Admin Intro with a second tile surface.
282
282
  // Installer and end-user use the native card component; security remains
283
283
  // enforced by route/instance filters and backend permissions.
284
284
  removeSafeOverview();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v96-nexowatt-native-shell-native-overview';
4
+ const VERSION = 'v98-nexowatt-native-shell-native-overview';
5
5
  const previous = window.NEXOWATT_NATIVE_SHELL;
6
6
  if (previous?.version === VERSION) return;
7
7
  previous?.destroy?.();
@@ -150,7 +150,8 @@
150
150
  const paper = document.getElementById('app-paper');
151
151
  const role = String(window.NEXOWATT_EOS_ACCESS_ROLE || 'unknown').toLowerCase();
152
152
  if (!paper || isLogin() || currentTab() !== 'tab-intro' || role === 'unknown') {
153
- document.getElementById('eos-native-overview-hero')?.remove();
153
+ const staleHero = document.getElementById('eos-native-overview-hero');
154
+ if (staleHero?.dataset.eosRuntimeOverviewHero === 'true') staleHero.remove();
154
155
  return;
155
156
  }
156
157
  const config = role === 'admin'
@@ -159,12 +160,18 @@
159
160
  ? { label: 'Installateur', text: 'Inbetriebnahme, Fehlersuche, Geräte- und EMS-Diagnose – mit deinen freigegebenen Installateurrechten.' }
160
161
  : { label: 'Endkunde', text: 'Energie, Laden, Gebäude und die aktuellen EMS-Entscheidungen auf einen Blick.' };
161
162
  let hero = document.getElementById('eos-native-overview-hero');
163
+ // A freshly built Intro.tsx owns this node. Do not rewrite React children.
164
+ if (hero?.dataset.eosNativeReactOverview === 'true') return;
165
+ // The bundled Admin-7 runtime predates the React hero. Supply one compatibility node until
166
+ // the next complete frontend rebuild; it uses the same role text and native-card surface.
162
167
  if (!hero) {
163
168
  hero = document.createElement('section');
164
169
  hero.id = 'eos-native-overview-hero';
165
170
  hero.className = 'eos-overview-hero eos-native-overview-hero';
171
+ hero.dataset.eosRuntimeOverviewHero = 'true';
166
172
  paper.insertBefore(hero, paper.firstChild || null);
167
173
  }
174
+ if (hero.dataset.eosRuntimeOverviewHero !== 'true') return;
168
175
  hero.innerHTML = `
169
176
  <div><span class="eos-overview-eyebrow">NexoWatt EOS</span><h1>Übersicht</h1><p>${config.text}</p></div>
170
177
  <div class="eos-overview-role" data-nexowatt-overview-role="${role}"><span class="eos-overview-status-dot"></span>${config.label}</div>`;
@@ -1,79 +1,45 @@
1
- /* NexoWatt EOS stable runtime v96 */
1
+ /* NexoWatt EOS stable runtime v98 */
2
2
  (() => {
3
3
  'use strict';
4
4
  const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
5
5
  const ASSIST_TEXT = /EOS\s*Assist/i;
6
6
  const PASSWORDLESS_TEXT = /Erstanmeldung\s+ohne\s+Passwort|first\s+sign-?in\s+without\s+password/i;
7
7
  const isLogin = () => /(?:\?|&)login(?:[=&]|$)/i.test(location.search) || /\/login(?:[/?#]|$)/i.test(location.pathname + location.hash);
8
- const visible = el => !!el && el.getClientRects().length > 0;
8
+ const visible = element => !!element && element.getClientRects().length > 0;
9
9
  const findInput = kind => {
10
10
  const inputs = [...document.querySelectorAll('input')].filter(visible);
11
- if (kind === 'password') return inputs.find(i => i.type === 'password');
12
- return inputs.find(i => i.type !== 'password' && /user|login|name/i.test(`${i.name} ${i.id} ${i.autocomplete} ${i.getAttribute('aria-label')||''}`)) || inputs.find(i => i.type !== 'password');
11
+ if (kind === 'password') return inputs.find(input => input.type === 'password');
12
+ return inputs.find(input => input.type !== 'password' && /user|login|name/i.test(`${input.name} ${input.id} ${input.autocomplete} ${input.getAttribute('aria-label') || ''}`)) || inputs.find(input => input.type !== 'password');
13
13
  };
14
- const findSubmit = () => [...document.querySelectorAll('button,input[type="submit"]')].find(el => visible(el) && (/anmelden|login|sign in/i.test((el.textContent||el.value||'').trim()) || el.type === 'submit'));
15
- const commonAncestor = (a,b) => {
16
- if (!a || !b) return null;
17
- const parents = new Set(); let n=a;
18
- while(n){parents.add(n);n=n.parentElement;}
19
- n=b; while(n&&!parents.has(n))n=n.parentElement;
20
- return n;
21
- };
22
- const markLogin = () => {
14
+ const findSubmit = () => [...document.querySelectorAll('button,input[type="submit"]')].find(element => visible(element) && (/anmelden|login|sign in/i.test((element.textContent || element.value || '').trim()) || element.type === 'submit'));
15
+ const sync = () => {
23
16
  if (!isLogin()) return;
24
- document.documentElement.classList.add('nw-eos-login-v96');
25
- document.body?.classList.add('nw-eos-login-v96');
26
- const u=findInput('user'), p=findInput('password'), submit=findSubmit();
27
- if (u && p) {
28
- let card=commonAncestor(u,p);
29
- while(card && card.parentElement && card.querySelectorAll('input').length < 2) card=card.parentElement;
30
- if (card) {
31
- card.setAttribute('data-nw-login-card','true');
32
- let outer=card.parentElement;
33
- for(let i=0;i<3 && outer?.parentElement;i++){
34
- if(outer.getBoundingClientRect().width > card.getBoundingClientRect().width+20) break;
35
- outer=outer.parentElement;
36
- }
37
- outer?.setAttribute('data-nw-login-shell','true');
38
- }
39
- const sync=()=>{
40
- const name=(u.value||'').trim().toLowerCase();
41
- const pass=p.value||'';
42
- if(submit && MANAGED_USERS.has(name) && !pass){
43
- submit.disabled=true;
44
- submit.setAttribute('aria-disabled','true');
45
- }
46
- };
47
- if(!u.dataset.nwV96){u.dataset.nwV96='1';u.addEventListener('input',sync);p.addEventListener('input',sync);sync();}
17
+ document.documentElement.classList.add('nw-eos-login-v98');
18
+ document.body?.classList.add('nw-eos-login-v98');
19
+ const username = findInput('user');
20
+ const password = findInput('password');
21
+ const submit = findSubmit();
22
+ const account = String(username?.value || '').trim().toLowerCase().replace(/^system\.user\./, '');
23
+ if (submit && MANAGED_USERS.has(account) && !String(password?.value || '')) {
24
+ submit.disabled = true;
25
+ submit.setAttribute('aria-disabled', 'true');
48
26
  }
49
27
  };
50
28
  const hideLegacyUi = () => {
51
- for (const el of document.querySelectorAll('button,a,[role="button"],div,span')) {
52
- const text=(el.textContent||'').trim();
53
- if(!text || text.length>100) continue;
54
- if(ASSIST_TEXT.test(text) || PASSWORDLESS_TEXT.test(text)){
55
- const target=el.closest('button,a,[role="button"],[data-eos-assist],.eos-assist') || el;
56
- target.style.setProperty('display','none','important');
57
- target.setAttribute('aria-hidden','true');
29
+ for (const element of document.querySelectorAll('button,a,[role="button"],div,span')) {
30
+ const value = (element.textContent || '').trim();
31
+ if (!value || value.length > 100) continue;
32
+ if (ASSIST_TEXT.test(value) || PASSWORDLESS_TEXT.test(value)) {
33
+ const target = element.closest('button,a,[role="button"],[data-eos-assist],.eos-assist') || element;
34
+ target.style.setProperty('display', 'none', 'important');
35
+ target.setAttribute('aria-hidden', 'true');
58
36
  }
59
37
  }
60
38
  };
61
- const apply=()=>{markLogin();hideLegacyUi();};
62
- const style=document.createElement('style');
63
- style.id='nw-eos-stable-v96-css';
64
- style.textContent=`
65
- html.nw-eos-login-v96,body.nw-eos-login-v96{min-height:100%;margin:0!important;overflow-x:hidden!important;overflow-y:auto!important}
66
- .nw-eos-login-v96 [data-nw-login-shell="true"]{width:min(520px,calc(100vw - 28px))!important;min-height:590px!important;height:auto!important;max-height:none!important;overflow:visible!important;box-sizing:border-box!important;padding:22px!important}
67
- .nw-eos-login-v96 [data-nw-login-card="true"]{width:min(460px,calc(100vw - 56px))!important;min-height:540px!important;height:auto!important;max-height:none!important;overflow:visible!important;box-sizing:border-box!important;padding-bottom:26px!important}
68
- .nw-eos-login-v96 [data-nw-login-card="true"] *{scrollbar-width:none}
69
- .nw-eos-login-v96 [data-nw-login-card="true"] *::-webkit-scrollbar{display:none!important;width:0!important;height:0!important}
70
- .nw-eos-login-v96 [data-nw-login-card="true"] [role="alert"]{position:static!important;max-height:none!important;overflow:visible!important;white-space:normal!important}
71
- [data-eos-assist],.eos-assist,.nexowatt-eos-assist{display:none!important}
72
- @media(max-height:620px){.nw-eos-login-v96 [data-nw-login-shell="true"]{min-height:0!important;margin:16px auto!important}.nw-eos-login-v96 [data-nw-login-card="true"]{min-height:0!important}}
73
- `;
74
- (document.head||document.documentElement).appendChild(style);
75
- const observer=new MutationObserver(()=>requestAnimationFrame(apply));
76
- observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['class','style']});
77
- if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',apply,{once:true});else apply();
78
- setInterval(apply,1500);
39
+ const apply = () => { sync(); hideLegacyUi(); };
40
+ const observer = new MutationObserver(() => requestAnimationFrame(apply));
41
+ observer.observe(document.documentElement, { subtree: true, childList: true });
42
+ document.addEventListener('input', apply, true);
43
+ document.addEventListener('change', apply, true);
44
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply, { once: true }); else apply();
79
45
  })();
@@ -0,0 +1,33 @@
1
+ (() => {
2
+ 'use strict';
3
+ const VERSION = 'v98-scroll-login-password-write';
4
+ const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
5
+ const normalizeUser = value => String(value || '').trim().toLowerCase().replace(/^system\.user\./, '');
6
+ const apply = () => {
7
+ document.documentElement.classList.add('eos-assist-disabled');
8
+ document.querySelectorAll('.eos-assist-root,.eos-assist-header-root,[data-eos-assist-root],.eos-passwordless-launcher')
9
+ .forEach(element => element.remove());
10
+ const username = document.querySelector('input#username,input[name="username"]');
11
+ const password = document.querySelector('input#password,input[name="password"]');
12
+ const submit = document.querySelector('button[type="submit"],input[type="submit"]');
13
+ if (submit && MANAGED_USERS.has(normalizeUser(username?.value)) && !String(password?.value || '')) {
14
+ submit.disabled = true;
15
+ submit.classList.add('Mui-disabled');
16
+ submit.setAttribute('aria-disabled', 'true');
17
+ }
18
+ };
19
+ let scheduled = false;
20
+ const schedule = () => {
21
+ if (scheduled) return;
22
+ scheduled = true;
23
+ queueMicrotask(() => { scheduled = false; apply(); });
24
+ };
25
+ const start = () => {
26
+ apply();
27
+ new MutationObserver(schedule).observe(document.documentElement, { subtree: true, childList: true });
28
+ document.addEventListener('input', schedule, true);
29
+ document.addEventListener('change', schedule, true);
30
+ };
31
+ document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, { once: true }) : start();
32
+ window.NEXOWATT_EOS_STABLE_V98 = Object.freeze({ version: VERSION, refresh: apply });
33
+ })();