iobroker.eos-admin 7.9.93 → 7.9.96

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 (55) hide show
  1. package/INSTALL_TEST_V7.9.96.md +11 -0
  2. package/MERGE_UPDATE_README_V7.9.96.md +5 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +19 -19
  4. package/NexoWatt_EOS_Admin_EMS_OVERVIEW_MERGE_VALIDATION.md +44 -0
  5. package/PUBLISH_STABLE_V7.9.96.md +10 -0
  6. package/RC73_EOS_ADMIN_MERGE_VALIDATION.md +78 -0
  7. package/README_EMS_OVERVIEW_MERGE.md +45 -0
  8. package/README_STABILITY_V7.9.96.md +11 -0
  9. package/RELEASE_ACCEPTANCE_V7.9.96.md +10 -0
  10. package/RELEASE_NOTES_V7.9.96.md +12 -0
  11. package/VALIDATION_EMS_OVERVIEW_MERGE.md +35 -0
  12. package/adminWww/assets/Adapters-B5_jQ7DE-v84.js +1 -1
  13. package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
  14. package/adminWww/assets/index-D2ymscJA-v84.js +1 -1
  15. package/adminWww/css/eos-ems-overview.css +62 -0
  16. package/adminWww/css/nexowatt-native-shell.css +104 -158
  17. package/adminWww/index.html +15 -11
  18. package/adminWww/js/eos-account-management.js +1 -1
  19. package/adminWww/js/eos-assistant.js +5 -556
  20. package/adminWww/js/eos-basic-settings.js +1 -1
  21. package/adminWww/js/eos-branding-sanitizer.js +1 -3
  22. package/adminWww/js/eos-ems-overview.js +140 -0
  23. package/adminWww/js/eos-native-security.js +1 -1
  24. package/adminWww/js/eos-policy-client.js +1 -1
  25. package/adminWww/js/eos-role-bootstrap.js +4 -4
  26. package/adminWww/js/eos-role-ui.js +34 -38
  27. package/adminWww/js/nexowatt-native-shell.js +11 -6
  28. package/adminWww/static/js/nexowatt-stable-login.js +79 -0
  29. package/adminWww/static/js/nexowatt-stable-v96.js +82 -0
  30. package/build/lib/chat/assistantKnowledge.js +6 -21
  31. package/build/lib/chat/chatOrchestrator.js +38 -33
  32. package/build/lib/chat/mcpClientManager.js +1 -1
  33. package/build/lib/nexowattStableAuth.js +74 -0
  34. package/build/lib/web.js +24 -7
  35. package/build/main.js +123 -308
  36. package/io-package.json +25 -16
  37. package/package.json +32 -15
  38. package/tools/nexowatt-account-management-selftest.cjs +2 -3
  39. package/tools/nexowatt-apply-ems-overview.cjs +115 -0
  40. package/tools/nexowatt-clean-legacy-runtime.cjs +4 -0
  41. package/tools/nexowatt-ems-overview-selftest.cjs +35 -0
  42. package/tools/nexowatt-internal-reserve-selftest.cjs +1 -3
  43. package/tools/nexowatt-login-layout-selftest.cjs +43 -22
  44. package/tools/nexowatt-merge-update-selftest.cjs +69 -0
  45. package/tools/nexowatt-modern-ui-selftest.cjs +4 -5
  46. package/tools/nexowatt-stable-v96-selftest.cjs +13 -0
  47. package/tools/nexowatt-sync-release-version.cjs +181 -0
  48. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +16 -0
  49. package/tools/nexowatt-validate-package.cjs +41 -10
  50. package/tools/nexowatt-version-sync-selftest.cjs +86 -0
  51. package/INSTALL_TEST_V7.9.93.md +0 -12
  52. package/PUBLISH_STABLE_V7.9.93.md +0 -19
  53. package/README_STABILITY_V7.9.93.md +0 -15
  54. package/RELEASE_ACCEPTANCE_V7.9.93.md +0 -15
  55. package/RELEASE_NOTES_V7.9.93.md +0 -10
@@ -0,0 +1,140 @@
1
+ (() => {
2
+ 'use strict';
3
+ const VERSION = 'v96-nexowatt-ems-overview-runtime';
4
+ const CONTRACT = 'nexowatt-ems-overview-v1';
5
+ const ROOT_ID = 'eos-ems-overview-runtime';
6
+ const POLL_MS = 5000;
7
+ const STALE_MS = 20000;
8
+ const COLORS = { ok: '#01bc69', info: '#48b9ff', warning: '#ffbd59', error: '#ff5f72' };
9
+ let timer = 0;
10
+ let inFlight = false;
11
+ let destroyed = false;
12
+
13
+ const esc = value => String(value ?? '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[character]));
14
+ const text = (value, fallback = '') => String(value ?? fallback).replace(/\s+/g, ' ').trim() || fallback;
15
+ 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
+ };
27
+ const currentTab = () => String(location.hash || '').match(/tab-[a-z0-9_-]+/i)?.[0]?.toLowerCase() || 'tab-intro';
28
+ const currentRole = () => String(window.NEXOWATT_EOS_ACCESS_ROLE || 'admin').toLowerCase();
29
+ 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;
46
+ const stateVal = (states, id) => states?.[id]?.val;
47
+
48
+ const normalizeInstances = (states, aliveStates) => {
49
+ const prefixes = new Set();
50
+ Object.keys(states || {}).forEach(id => { const match = id.match(/^(nexowatt-ui\.\d+)\.info\.adminOverview\./); if (match) prefixes.add(match[1]); });
51
+ return Array.from(prefixes).map(instance => {
52
+ const summaryId = `${instance}.info.adminOverview.summaryJson`;
53
+ const summary = parse(stateVal(states, summaryId), {});
54
+ const events = parse(stateVal(states, `${instance}.info.adminOverview.eventsJson`), []);
55
+ const updatedAt = Math.max(num(summary.updatedAt), num(summary.generatedAt), num(stateVal(states, `${instance}.info.adminOverview.updatedAt`)), num(states?.[summaryId]?.ts));
56
+ const rawAlive = stateVal(aliveStates, `system.adapter.${instance}.alive`);
57
+ const alive = rawAlive == null ? null : (rawAlive === true || rawAlive === 1 || ['true', '1', 'online', 'active'].includes(String(rawAlive).toLowerCase()));
58
+ return { instance, alive, summary: { ...summary, status: severity(summary.status || stateVal(states, `${instance}.info.adminOverview.status`)), headline: text(summary.headline || stateVal(states, `${instance}.info.adminOverview.headline`), 'EMS-Diagnose wird aufgebaut'), reason: text(summary.reason || stateVal(states, `${instance}.info.adminOverview.reason`), ''), binding: text(summary.binding || stateVal(states, `${instance}.info.adminOverview.binding`), 'none') }, events: Array.isArray(events) ? events : [], updatedAt };
59
+ }).sort((a, b) => Number(b.alive === true) - Number(a.alive === true) || b.updatedAt - a.updatedAt || a.instance.localeCompare(b.instance));
60
+ };
61
+
62
+ const ensureRoot = () => {
63
+ const reactCard = document.querySelector(`[data-nexowatt-contract="${CONTRACT}"]:not(#${ROOT_ID})`);
64
+ if (reactCard) { document.getElementById(ROOT_ID)?.remove(); return null; }
65
+ const paper = document.getElementById('app-paper');
66
+ if (!paper || currentTab() !== 'tab-intro') return null;
67
+ let root = document.getElementById(ROOT_ID);
68
+ if (!root) {
69
+ root = document.createElement('section');
70
+ root.id = ROOT_ID;
71
+ root.className = 'eos-ems-overview-card';
72
+ root.dataset.nexowattContract = CONTRACT;
73
+ }
74
+ const hero = document.getElementById('eos-native-overview-hero');
75
+ if (hero?.parentElement === paper) paper.insertBefore(root, hero.nextSibling); else paper.insertBefore(root, paper.firstChild || null);
76
+ return root;
77
+ };
78
+
79
+ const unavailable = message => {
80
+ const root = ensureRoot(); if (!root) return;
81
+ 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>`;
83
+ };
84
+
85
+ const fallbackDecisions = summary => {
86
+ const result = []; const charging = summary.charging || {}; const storage = summary.storage || {};
87
+ if (charging.available) result.push({ severity: num(charging.faultCount) > 0 ? 'error' : num(charging.waitingCount) > 0 ? 'info' : 'ok', title: `${num(charging.activeCount)} lädt · ${num(charging.waitingCount)} wartet · ${num(charging.faultCount)} gestört`, reason: text(charging.limiterText || charging.status, 'Lademanagement aktiv'), details: `Ist ${formatPower(charging.actualW)} · Soll ${formatPower(charging.targetW)}` });
88
+ if (storage.available) result.push({ severity: storage.writeOk === false ? 'error' : 'ok', title: `Speicher ${text(storage.topology, 'aktiv')}${storage.socPct != null ? ` · SoC ${Math.round(num(storage.socPct))} %` : ''}`, reason: text(storage.reason, 'Speicherregelung aktiv'), details: `Ist ${formatPower(storage.actualW)} · Soll ${formatPower(storage.targetW)}` });
89
+ return result;
90
+ };
91
+
92
+ const render = data => {
93
+ const root = ensureRoot(); if (!root) return;
94
+ 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
+ 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);
97
+ 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
+ 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
+ const tags = [];
100
+ if (text(summary.binding, 'none') !== 'none') tags.push({ label: `Bindend: ${text(budget.bindingText || summary.binding)}`, warning: true });
101
+ if (para14a.communicationFallbackActive) tags.push({ label: `§14a-Fallback ${formatPower(para14a.fallbackCapW)}`, warning: true }); else if (para14a.binding) tags.push({ label: '§14a begrenzt', warning: true });
102
+ if (peak.active) tags.push({ label: `Peak-Shaving: ${text(peak.status, 'aktiv')}`, warning: true });
103
+ if (tariff.available) tags.push({ label: `Tarif: ${text(tariff.state, 'aktiv')}${tariff.priceEurPerKwh != null ? ` · ${num(tariff.priceEurPerKwh).toFixed(3)} €/kWh` : ''}` });
104
+ if (forecast.available) tags.push({ label: `Prognose: ${text(forecast.source, 'aktiv')}${forecast.fresh === false ? ' · veraltet' : ''}`, warning: forecast.fresh === false });
105
+ if (ems.safetyActive || ems.safetyEmergencyStop || ems.safetyValid === false) tags.push({ label: 'EOS Safety aktiv', warning: true });
106
+ const detailsPort = Math.max(1, num(summary.details?.port, 8188)); const detailsPath = text(summary.details?.path, '/ems-apps.html?tab=status'); const detailsUrl = `${location.protocol}//${location.hostname}:${detailsPort}${detailsPath.startsWith('/') ? detailsPath : `/${detailsPath}`}`;
107
+ const metrics = `<div class="eos-ems-overview-metric"><span>EMS-Budget</span><strong>${totalBudget > 0 ? esc(formatPower(totalBudget)) : 'nicht begrenzt'}</strong><small>Rest ${esc(formatPower(remaining))}</small></div><div class="eos-ems-overview-metric"><span>Lademanagement</span><strong>${esc(formatPower(charging.actualW))} / ${esc(formatPower(charging.targetW))}</strong><small>${num(charging.activeCount)} lädt · ${num(charging.waitingCount)} wartet</small></div>${storage.available ? `<div class="eos-ems-overview-metric"><span>Speicher ${esc(text(storage.topology))}</span><strong>${esc(formatPower(storage.actualW))} / ${esc(formatPower(storage.targetW))}</strong><small>${storage.socPct != null ? `SoC ${Math.round(num(storage.socPct))} %` : esc(text(storage.reason, 'aktiv'))}</small></div>` : `<div class="eos-ems-overview-metric"><span>PV-Budget</span><strong>${esc(formatPower(budget.pvBudgetW))}</strong><small>PV-Rest ${esc(formatPower(budget.remainingPvW))}</small></div>`}<div class="eos-ems-overview-metric"><span>Letzter Regeltick</span><strong>${esc(formatAge(ems.lastTickTs || data.updatedAt))}</strong><small>${isTechnical ? `Zyklus ${num(ems.cycleMs)} ms` : esc(text(ems.decision, 'EMS aktiv'))}</small></div>`;
108
+ const decisionsHtml = decisions.length ? decisions.map(item => `<div class="eos-ems-overview-decision eos-ems-overview-decision--${severity(item.severity)}"><strong>${esc(text(item.title, 'EMS-Entscheidung'))}</strong><span>${esc(text(item.reason, 'Keine zusätzliche Begrenzung'))}</span>${isTechnical && item.details ? `<small>${esc(item.details)}</small>` : ''}</div>`).join('') : '<div class="eos-ems-overview-empty">Keine aktiven Regelentscheidungen. EOS arbeitet mit den aktuellen Messwerten.</div>';
109
+ const eventsHtml = isTechnical ? (events.length ? events.map(item => `<div class="eos-ems-overview-event eos-ems-overview-event--${severity(item.severity)}"><time>${esc(formatClock(item.ts))}</time><div><strong>${esc(text(item.title, 'EMS'))}</strong><span>${esc(text(item.message || item.reason, 'Zustand aktualisiert'))}</span></div></div>`).join('') : '<div class="eos-ems-overview-empty">Noch keine Zustandsänderung im begrenzten Ereignispuffer.</div>') : `<div class="eos-ems-overview-decision eos-ems-overview-decision--info"><strong>${esc(text(summary.headline, 'EMS aktiv'))}</strong><span>${esc(text(summary.reason, 'EOS optimiert die Anlage innerhalb aller Sicherheitsgrenzen.'))}</span></div>`;
110
+ 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
+ };
112
+
113
+ const load = async () => {
114
+ if (destroyed || inFlight || document.visibilityState === 'hidden' || currentTab() !== 'tab-intro') return;
115
+ const socket = getSocket();
116
+ if (!socket?.getForeignStates) { unavailable('Die Admin-Verbindung wird noch aufgebaut.'); return; }
117
+ inFlight = true;
118
+ try {
119
+ const [states, alive] = await Promise.all([socket.getForeignStates('nexowatt-ui.*.info.adminOverview.*'), socket.getForeignStates('system.adapter.nexowatt-ui.*.alive')]);
120
+ 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; }
124
+ };
125
+
126
+ const apply = () => {
127
+ document.getElementById('eos-role-safe-overview')?.remove();
128
+ document.documentElement.classList.remove('eos-safe-overview-active');
129
+ if (currentTab() !== 'tab-intro') { document.getElementById(ROOT_ID)?.remove(); return; }
130
+ ensureRoot(); void load();
131
+ };
132
+ const start = () => {
133
+ apply();
134
+ timer = window.setInterval(apply, POLL_MS);
135
+ window.addEventListener('hashchange', apply);
136
+ document.addEventListener('visibilitychange', apply);
137
+ };
138
+ 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(); } });
140
+ })();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v93-product-role-access';
4
+ const VERSION = 'v91-product-role-access';
5
5
  const protectedAdapters = new Set([
6
6
  'admin', 'eos-admin', 'backitup', 'nexowatt-devices', 'nexowatt-device', 'nexowatt-dev', 'nexowatt-ui',
7
7
  ]);
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v93-role-access';
4
+ const VERSION = 'v91-role-access';
5
5
  const existing = window.NEXOWATT_EOS_POLICY_CLIENT;
6
6
  if (existing?.version === VERSION) return;
7
7
  existing?.destroy?.();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v93-final-first-login-submit-guard';
4
+ const VERSION = 'v96-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;
@@ -172,7 +172,7 @@
172
172
  overlay.querySelector('.eos-first-login-submit').textContent = t.submit;
173
173
  overlay.querySelector('.eos-first-login-logout').textContent = t.logout;
174
174
  const form = overlay.querySelector('form');
175
- form.elements.username.value = claim.userName || 'guest';
175
+ form.elements.username.value = userName;
176
176
  const password = form.elements.password;
177
177
  const repeat = form.elements.passwordRepeat;
178
178
  const show = form.elements.show;
@@ -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
- // v93 compact normal-login first activation: do not add role buttons or a second panel.
503
+ // v96 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());
@@ -631,7 +631,7 @@
631
631
 
632
632
  const contextUrl = new URL('nexowatt/security/context', base).href;
633
633
  const controller = new AbortController();
634
- const timeout = window.setTimeout(() => controller.abort(), 3500);
634
+ const timeout = window.setTimeout(() => controller.abort(), 8000);
635
635
 
636
636
  window.NEXOWATT_EOS_BOOTSTRAP_READY = fetch(contextUrl, {
637
637
  credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' }, signal: controller.signal,
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v93-final-clickable-overview-live-reserve-filter';
4
+ const VERSION = 'v96-native-overview-ems-live-reserve-filter';
5
5
  window.NEXOWATT_EOS_ROLE_UI_VERSION = VERSION;
6
6
 
7
7
  const state = {
@@ -251,47 +251,43 @@
251
251
  if (/permissionerror|cannot get data/i.test(node.textContent || '')) node.classList.add('eos-role-hidden');
252
252
  });
253
253
  };
254
- const ensureSafeOverview = () => safe(() => {
255
- if (isLoginView() || state.role === 'admin' || state.role === 'unknown' || currentRoute() !== 'tab-intro') {
256
- removeSafeOverview();
257
- return;
254
+ const updateNativeOverviewRole = () => {
255
+ if (isLoginView() || currentRoute() !== 'tab-intro') return;
256
+ const config = state.role === 'admin'
257
+ ? { label: 'Admin / Service', text: 'Systemstatus, Module, Dienste und Anlagenzugänge in einer modernen Serviceübersicht.' }
258
+ : state.role === 'installer'
259
+ ? { label: 'Installateur', text: 'Inbetriebnahme, Fehlersuche, Geräte- und EMS-Diagnose – mit deinen freigegebenen Installateurrechten.' }
260
+ : { label: 'Endkunde', text: 'Energie, Laden, Gebäude und die aktuellen EMS-Entscheidungen auf einen Blick.' };
261
+ const hero = document.getElementById('eos-native-overview-hero');
262
+ const description = hero?.querySelector('p');
263
+ const role = hero?.querySelector('.eos-overview-role');
264
+ if (description) description.textContent = config.text;
265
+ if (role) {
266
+ role.innerHTML = '<span class="eos-overview-status-dot"></span>';
267
+ role.append(document.createTextNode(config.label));
268
+ role.setAttribute('data-nexowatt-overview-role', state.role);
258
269
  }
270
+ };
271
+ const hideIntroEditControls = () => {
259
272
  const paper = appPaper();
260
273
  if (!paper) return;
261
- document.documentElement.classList.add('eos-safe-overview-active');
262
- suppressPermissionErrors();
263
- let overview = document.getElementById('eos-role-safe-overview');
264
- if (!overview) {
265
- overview = document.createElement('section');
266
- overview.id = 'eos-role-safe-overview';
267
- overview.className = 'eos-role-safe-overview';
268
- paper.appendChild(overview);
269
- }
270
- const installer = state.role === 'installer';
271
- const actions = actionDefinitions(state.role).filter(([, , tab]) => tab === 'basic-settings' || isRouteAllowed(state.role, tab));
272
- overview.innerHTML = `
273
- <header class="eos-overview-hero eos-role-overview-hero">
274
- <div><span class="eos-overview-eyebrow">NexoWatt EOS</span><h1>Übersicht</h1><p>${installer ? 'Inbetriebnahme, Fehlersuche und Anlagenkonfiguration – passend zu deinen Installateurrechten.' : 'Deine freigegebenen Energie-, Smart-Home- und Sicherungsfunktionen.'}</p></div>
275
- <div class="eos-overview-role"><span class="eos-overview-status-dot"></span>${installer ? 'Installateur' : 'Gast / Endkunde'}</div>
276
- </header>
277
- <div class="eos-overview-grid">${actions.map(([label, description, tab, icon]) => `
278
- <a class="eos-overview-action" data-eos-role-tab="${tab}" href="${tab === 'basic-settings' ? '#tab-intro' : `#${tab}`}">
279
- <span class="eos-overview-action-icon" aria-hidden="true">${iconFor(icon)}</span>
280
- <span><strong>${label}</strong><small>${description}</small></span><i aria-hidden="true">›</i>
281
- </a>`).join('')}</div>`;
282
- overview.querySelectorAll('a[data-eos-role-tab]').forEach(link => {
283
- link.addEventListener('click', event => {
284
- event.preventDefault();
285
- event.stopPropagation();
286
- const targetTab = link.getAttribute('data-eos-role-tab') || '';
287
- if (targetTab) navigateToTab(targetTab, link.textContent || '');
288
- });
289
- link.addEventListener('keydown', event => {
290
- if (event.key !== 'Enter' && event.key !== ' ') return;
291
- event.preventDefault();
292
- navigateToTab(link.getAttribute('data-eos-role-tab') || '', link.textContent || '');
293
- });
274
+ const admin = state.role === 'admin';
275
+ paper.querySelectorAll(':scope > .MuiFab-root,:scope > button.MuiFab-root').forEach(button => {
276
+ button.classList.toggle('eos-intro-admin-edit-hidden', !admin);
277
+ if (!admin) button.setAttribute('aria-hidden', 'true'); else button.removeAttribute('aria-hidden');
294
278
  });
279
+ };
280
+ const ensureSafeOverview = () => safe(() => {
281
+ // 7.9.96: never cover the real Admin Intro with a second tile surface.
282
+ // Installer and end-user use the native card component; security remains
283
+ // enforced by route/instance filters and backend permissions.
284
+ removeSafeOverview();
285
+ if (isLoginView() || state.role === 'unknown' || currentRoute() !== 'tab-intro') return;
286
+ suppressPermissionErrors();
287
+ updateNativeOverviewRole();
288
+ hideIntroEditControls();
289
+ window.NEXOWATT_NATIVE_SHELL?.refresh?.();
290
+ window.NEXOWATT_EOS_EMS_OVERVIEW?.refresh?.();
295
291
  });
296
292
 
297
293
  const hideOfficialReserveSurfaces = () => safe(() => {
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v93-nexowatt-native-shell-modern-overview';
4
+ const VERSION = 'v96-nexowatt-native-shell-native-overview';
5
5
  const previous = window.NEXOWATT_NATIVE_SHELL;
6
6
  if (previous?.version === VERSION) return;
7
7
  previous?.destroy?.();
@@ -148,21 +148,26 @@
148
148
 
149
149
  const ensureModernOverview = () => {
150
150
  const paper = document.getElementById('app-paper');
151
- const role = window.NEXOWATT_EOS_ACCESS_ROLE || 'unknown';
152
- if (!paper || isLogin() || currentTab() !== 'tab-intro' || role !== 'admin') {
151
+ const role = String(window.NEXOWATT_EOS_ACCESS_ROLE || 'unknown').toLowerCase();
152
+ if (!paper || isLogin() || currentTab() !== 'tab-intro' || role === 'unknown') {
153
153
  document.getElementById('eos-native-overview-hero')?.remove();
154
154
  return;
155
155
  }
156
+ const config = role === 'admin'
157
+ ? { label: 'Admin / Service', text: 'Systemstatus, Module, Dienste und Anlagenzugänge in einer modernen Serviceübersicht.' }
158
+ : role === 'installer' || role === 'installateur'
159
+ ? { label: 'Installateur', text: 'Inbetriebnahme, Fehlersuche, Geräte- und EMS-Diagnose – mit deinen freigegebenen Installateurrechten.' }
160
+ : { label: 'Endkunde', text: 'Energie, Laden, Gebäude und die aktuellen EMS-Entscheidungen auf einen Blick.' };
156
161
  let hero = document.getElementById('eos-native-overview-hero');
157
162
  if (!hero) {
158
163
  hero = document.createElement('section');
159
164
  hero.id = 'eos-native-overview-hero';
160
165
  hero.className = 'eos-overview-hero eos-native-overview-hero';
161
- hero.innerHTML = `
162
- <div><span class="eos-overview-eyebrow">NexoWatt EOS</span><h1>Übersicht</h1><p>Systemstatus, Module, Dienste und Anlagenzugänge in einer modernen Serviceübersicht.</p></div>
163
- <div class="eos-overview-role"><span class="eos-overview-status-dot"></span>Admin / Service</div>`;
164
166
  paper.insertBefore(hero, paper.firstChild || null);
165
167
  }
168
+ hero.innerHTML = `
169
+ <div><span class="eos-overview-eyebrow">NexoWatt EOS</span><h1>Übersicht</h1><p>${config.text}</p></div>
170
+ <div class="eos-overview-role" data-nexowatt-overview-role="${role}"><span class="eos-overview-status-dot"></span>${config.label}</div>`;
166
171
  };
167
172
 
168
173
  const apply = () => {
@@ -0,0 +1,79 @@
1
+ /* NexoWatt EOS stable runtime v96 */
2
+ (() => {
3
+ 'use strict';
4
+ const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
5
+ const ASSIST_TEXT = /EOS\s*Assist/i;
6
+ const PASSWORDLESS_TEXT = /Erstanmeldung\s+ohne\s+Passwort|first\s+sign-?in\s+without\s+password/i;
7
+ const isLogin = () => /(?:\?|&)login(?:[=&]|$)/i.test(location.search) || /\/login(?:[/?#]|$)/i.test(location.pathname + location.hash);
8
+ const visible = el => !!el && el.getClientRects().length > 0;
9
+ const findInput = kind => {
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');
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 = () => {
23
+ 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();}
48
+ }
49
+ };
50
+ 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');
58
+ }
59
+ }
60
+ };
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);
79
+ })();
@@ -0,0 +1,82 @@
1
+ (() => {
2
+ 'use strict';
3
+ const VERSION = 'v96-spacious-login-first-password-fix';
4
+ const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
5
+ const ASSIST_TEXT = /EOS\s*Assist/i;
6
+ const normalizeUser = value => String(value || '').trim().toLowerCase().replace(/^system\.user\./, '');
7
+ const styleId = 'nexowatt-stable-v96-style';
8
+
9
+ const ensureStyle = () => {
10
+ if (document.getElementById(styleId)) return;
11
+ const style = document.createElement('style');
12
+ style.id = styleId;
13
+ style.textContent = `
14
+ html.eos-login,html.eos-login body{margin:0!important;overflow-x:hidden!important;overflow-y:auto!important}
15
+ html.eos-login main.MuiPaper-root{box-sizing:border-box!important}
16
+ html.eos-login main.MuiPaper-root::before{width:min(520px,calc(100vw - 24px))!important;min-height:590px!important;height:min(640px,calc(100dvh - 24px))!important}
17
+ html.eos-login .eos-login-card-modern,
18
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),
19
+ html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password),
20
+ html.eos-login [data-nw-login-card="true"]{
21
+ width:min(460px,calc(100vw - 56px))!important;
22
+ min-height:540px!important;
23
+ height:auto!important;
24
+ max-width:460px!important;
25
+ max-height:none!important;
26
+ padding:34px 36px 32px!important;
27
+ overflow:visible!important;
28
+ box-sizing:border-box!important
29
+ }
30
+ html.eos-login .MuiAlert-root,html.eos-login [role="alert"]{position:static!important;width:100%!important;max-width:100%!important;max-height:none!important;overflow:visible!important;white-space:normal!important;overflow-wrap:anywhere!important;flex-shrink:0!important}
31
+ html.eos-login .eos-login-card-modern *,html.eos-login [data-nw-login-card="true"] *{box-sizing:border-box;max-width:100%}
32
+ html.eos-assist-disabled .eos-assist-root,html.eos-assist-disabled .eos-assist-header-root,[data-eos-assist-root]{display:none!important}
33
+ @media(max-width:560px){html.eos-login .eos-login-card-modern,html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password),html.eos-login [data-nw-login-card="true"]{width:calc(100vw - 38px)!important;max-width:none!important;min-height:0!important;padding:26px 22px!important}}
34
+ @media(max-height:690px){html.eos-login .eos-login-card-modern,html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#username),html.eos-login main.MuiPaper-root > .MuiPaper-root:has(#password),html.eos-login [data-nw-login-card="true"]{min-height:0!important}}
35
+ `;
36
+ document.head.appendChild(style);
37
+ };
38
+
39
+ const findCard = (username, password) => username?.closest?.('.MuiPaper-root,form,main,section')
40
+ || password?.closest?.('.MuiPaper-root,form,main,section');
41
+ const findSubmit = card => card?.querySelector?.('button[type="submit"]')
42
+ || Array.from(card?.querySelectorAll?.('button') || []).find(button => /anmelden|login|sign in|aanmelden/i.test(button.textContent || ''));
43
+
44
+ const apply = () => {
45
+ ensureStyle();
46
+ document.documentElement.classList.add('eos-assist-disabled');
47
+ document.querySelectorAll('.eos-assist-root,.eos-assist-header-root,[data-eos-assist-root],.eos-passwordless-launcher')
48
+ .forEach(element => element.remove());
49
+ const username = document.querySelector('input#username,input[name="username"]');
50
+ const password = document.querySelector('input#password,input[name="password"]');
51
+ const name = normalizeUser(username?.value);
52
+ const pass = String(password?.value || '');
53
+ const card = findCard(username, password);
54
+ const submit = findSubmit(card);
55
+ if (submit && MANAGED_USERS.has(name) && !pass) {
56
+ submit.disabled = true;
57
+ submit.classList.add('Mui-disabled');
58
+ submit.setAttribute('aria-disabled', 'true');
59
+ }
60
+ document.querySelectorAll('body *').forEach(element => {
61
+ if (element.children.length) return;
62
+ const value = element.textContent || '';
63
+ if (/\[iobroker\.eos-admin@[^\]]+\]/i.test(value)) element.textContent = value.replace(/\[iobroker\.eos-admin@[^\]]+\]/ig, '[NexoWatt]');
64
+ if (ASSIST_TEXT.test(value) && element.closest?.('.eos-assist-root,.eos-assist-header-root')) element.closest('.eos-assist-root,.eos-assist-header-root')?.remove();
65
+ });
66
+ };
67
+
68
+ let scheduled = false;
69
+ const schedule = () => {
70
+ if (scheduled) return;
71
+ scheduled = true;
72
+ queueMicrotask(() => { scheduled = false; apply(); });
73
+ };
74
+ const start = () => {
75
+ apply();
76
+ new MutationObserver(schedule).observe(document.documentElement, { subtree: true, childList: true, characterData: true });
77
+ document.addEventListener('input', schedule, true);
78
+ document.addEventListener('change', schedule, true);
79
+ };
80
+ document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, { once: true }) : start();
81
+ window.NEXOWATT_EOS_STABLE_V96 = { version: VERSION, refresh: apply };
82
+ })();
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Curated "wiki" of NexoWatt EOS knowledge that is injected into the assistant's system prompt.
3
+ * Curated "wiki" of ioBroker how-to knowledge that is injected into the assistant's system prompt.
4
4
  *
5
5
  * This is meant to grow over time: add a new {@link KnowledgeSection} to {@link KNOWLEDGE_SECTIONS}
6
6
  * for each task the assistant should know how to guide the user through. Keep entries short and
@@ -10,21 +10,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.KNOWLEDGE_SECTIONS = void 0;
11
11
  exports.renderAssistantKnowledge = renderAssistantKnowledge;
12
12
  exports.KNOWLEDGE_SECTIONS = [
13
- {
14
- title: 'NexoWatt EOS product areas and role-aware support',
15
- body: [
16
- 'EOS Assist can answer questions about the live NexoWatt EOS system: Overview/Cockpit, Modules,',
17
- 'Services, Datapoints, Structure (rooms/functions), System logs, Access & Rights, NexoWatt EOS',
18
- 'applications, System hosts, Files and NexoWatt Backup. It can also inspect installed modules,',
19
- 'running services, object/state metadata, current values, device assignments, logs and history',
20
- 'through the available read tools. Always query real system data before making a system-specific claim.',
21
- 'Respect the authenticated role: Admin/Service may receive full system and service detail;',
22
- 'Installers may receive commissioning and troubleshooting detail available to their account;',
23
- 'End users receive only their permitted Smart Home, display and operating information.',
24
- 'Internal package names and compatibility identifiers are technical details; use NexoWatt EOS product',
25
- 'terminology in normal answers and show technical IDs only when they are required for a precise action.',
26
- ].join(' '),
27
- },
28
13
  {
29
14
  title: 'Open an adapter instance configuration',
30
15
  body: [
@@ -212,7 +197,7 @@ exports.KNOWLEDGE_SECTIONS = [
212
197
  {
213
198
  title: 'Run a script in the javascript adapter (run_javascript)',
214
199
  body: [
215
- 'To run logic WITH the full NexoWatt EOS scripting API, use run_javascript (act mode). It sends the',
200
+ 'To run logic WITH the full ioBroker scripting API, use run_javascript (act mode). It sends the',
216
201
  'code to the `javascript` adapter and runs it ONCE (not saved), returning { ok, error, output,',
217
202
  'logs }. Choose `language` "js" (default) or "ts". Inside the script the usual API is available:',
218
203
  '`getState("id").val`, `setState("id", value)`, `on({id, change:"ne"}, …)`, `schedule(...)`,',
@@ -225,10 +210,10 @@ exports.KNOWLEDGE_SECTIONS = [
225
210
  ].join('\n'),
226
211
  },
227
212
  {
228
- title: 'Automation options in NexoWatt EOS — pick the right tool',
213
+ title: 'Automation options in ioBroker — pick the right tool',
229
214
  body: [
230
- 'NexoWatt EOS can automate from simple to fully programmable. IMPORTANT: every option below needs a',
231
- 'SEPARATE adapter that is NOT part of a default NexoWatt EOS installation — `scenes`, `scheduler`,',
215
+ 'ioBroker can automate from simple to fully programmable. IMPORTANT: every option below needs a',
216
+ 'SEPARATE adapter that is NOT part of a default ioBroker installation — `scenes`, `scheduler`,',
232
217
  '`fullcalendar` and `javascript` (which provides Blockly, JavaScript and TypeScript). Before you',
233
218
  'recommend or use one, CHECK whether it is installed (list_adapters, and list_instances for a',
234
219
  'running instance). If it is missing, tell the user it must be installed first and offer',
@@ -286,7 +271,7 @@ exports.KNOWLEDGE_SECTIONS = [
286
271
  ].join('\n'),
287
272
  },
288
273
  {
289
- title: 'NexoWatt EOS JavaScript API (cheat-sheet for scripts and run_javascript)',
274
+ title: 'ioBroker JavaScript adapter API (cheat-sheet for scripts and run_javascript)',
290
275
  body: [
291
276
  'When you WRITE a script (for the Scripts tab or for run_javascript), use the ioBroker javascript',
292
277
  'API — it is NOT plain Node.js. Key functions:',