iobroker.eos-admin 7.9.89 → 7.9.93

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 (64) hide show
  1. package/INSTALL_TEST_V7.9.93.md +12 -0
  2. package/NEXOWATT_EOS_BUILD_INFO.json +19 -26
  3. package/PUBLISH_STABLE_V7.9.93.md +19 -0
  4. package/README_STABILITY_V7.9.93.md +15 -0
  5. package/RELEASE_ACCEPTANCE_V7.9.93.md +15 -0
  6. package/RELEASE_NOTES_V7.9.93.md +10 -0
  7. package/adminWww/assets/Adapters-B5_jQ7DE-v84.js +1 -1
  8. package/adminWww/assets/Instances-YdaGnS5a-v84.js +1 -1
  9. package/adminWww/css/nexowatt-native-shell.css +304 -13
  10. package/adminWww/index.html +11 -11
  11. package/adminWww/js/eos-account-management.js +1 -1
  12. package/adminWww/js/eos-assistant.js +487 -55
  13. package/adminWww/js/eos-basic-settings.js +1 -1
  14. package/adminWww/js/eos-branding-sanitizer.js +3 -1
  15. package/adminWww/js/eos-native-security.js +1 -1
  16. package/adminWww/js/eos-policy-client.js +1 -1
  17. package/adminWww/js/eos-role-bootstrap.js +180 -46
  18. package/adminWww/js/eos-role-ui.js +156 -23
  19. package/adminWww/js/nexowatt-native-shell.js +1 -1
  20. package/build/lib/chat/assistantKnowledge.js +21 -6
  21. package/build/lib/chat/chatOrchestrator.js +33 -38
  22. package/build/lib/chat/mcpClientManager.js +1 -1
  23. package/build/lib/web.js +39 -0
  24. package/build/main.js +307 -1
  25. package/io-package.json +23 -8
  26. package/package.json +21 -13
  27. package/tools/nexowatt-account-management-selftest.cjs +1 -1
  28. package/tools/nexowatt-ensure-release-defaults.cjs +72 -0
  29. package/tools/nexowatt-internal-reserve-selftest.cjs +4 -1
  30. package/tools/nexowatt-login-layout-selftest.cjs +62 -0
  31. package/tools/nexowatt-modern-ui-selftest.cjs +7 -5
  32. package/tools/nexowatt-release-defaults-selftest.cjs +29 -0
  33. package/tools/nexowatt-validate-package.cjs +18 -8
  34. package/INSTALL_TEST_V7.9.89.md +0 -73
  35. package/PUBLISH_STABLE_V7.9.89.md +0 -33
  36. package/README_STABILITY_V7.9.89.md +0 -26
  37. package/RELEASE_ACCEPTANCE_V7.9.89.md +0 -40
  38. package/admin/img/eos/nexowatt-192-maskable.png +0 -0
  39. package/admin/img/eos/nexowatt-24.png +0 -0
  40. package/admin/img/eos/nexowatt-512-maskable.png +0 -0
  41. package/admin/img/eos/nexowatt-eos-logo.png +0 -0
  42. package/adminWww/admin.png +0 -0
  43. package/adminWww/img/eos/nexowatt-24.png +0 -0
  44. package/adminWww/img/eos/nexowatt-eos-logo.png +0 -0
  45. package/adminWww/img/eos/nexowatt-header-128.png +0 -0
  46. package/adminWww/img/eos/nexowatt-header-192.png +0 -0
  47. package/adminWww/img/eos/nexowatt-header-256.png +0 -0
  48. package/adminWww/img/eos/nexowatt-header-512.png +0 -0
  49. package/adminWww/img/eos/nexowatt-header-64.png +0 -0
  50. package/adminWww/img/logo192.png +0 -0
  51. package/adminWww/js/eos-adapter-surface-cleanup.js +0 -141
  52. package/adminWww/js/eos-toolbar-ghost-cleanup.js +0 -102
  53. package/docs/NEXOWATT_EOS_BRANDING_DE.md +0 -29
  54. package/docs/NEXOWATT_EOS_UI_LOGO_LOGIN_V6_DE.md +0 -9
  55. package/docs/NEXOWATT_EOS_UI_PERFORMANCE_V5_DE.md +0 -25
  56. package/docs/NEXOWATT_EOS_UI_V12_HEADER_LOGO_NAV_CLEANUP_DE.md +0 -16
  57. package/docs/NEXOWATT_EOS_UI_V12_HEADER_NAV_LOGO_FIX_DE.md +0 -16
  58. package/docs/NEXOWATT_EOS_UI_V15_DE.md +0 -8
  59. package/docs/NEXOWATT_EOS_UI_V16_DE.md +0 -9
  60. package/docs/NEXOWATT_EOS_UI_V17_DE.md +0 -24
  61. package/docs/NEXOWATT_EOS_UI_V6_FIXES_DE.md +0 -13
  62. package/docs/NEXOWATT_EOS_UI_V6_LOGIN_LOGO_404_FIX_DE.md +0 -9
  63. package/docs/NEXOWATT_EOS_UI_V6_LOGO_LOGIN_FIX_DE.md +0 -10
  64. package/docs/NEXOWATT_EOS_UI_V7_RESTORE_LOGO_SCOPE_FIX_DE.md +0 -18
@@ -1,42 +1,410 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v89-header-eos-assist';
4
+ const VERSION = 'v93-header-eos-assist-live-system';
5
5
  const previous = window.NEXOWATT_EOS_ASSIST;
6
6
  if (previous?.version === VERSION) return;
7
7
  previous?.destroy?.();
8
8
 
9
- const scriptUrl = document.currentScript?.src || document.querySelector('script[src*="eos-assistant.js"]')?.src || location.href;
9
+ const scriptUrl =
10
+ document.currentScript?.src || document.querySelector('script[src*="eos-assistant.js"]')?.src || location.href;
10
11
  const logoUrl = new URL('../img/eos/nexowatt-192.png', scriptUrl).href;
11
12
  const abort = new AbortController();
12
13
  let unsubscribeDom = null;
13
- let timer = 0;
14
+ let reconnectTimer = 0;
14
15
  let destroyed = false;
16
+ let socket = null;
17
+ let status = null;
18
+ let settings = {
19
+ provider: 'anthropic',
20
+ model: '',
21
+ credentialId: '',
22
+ baseUrl: '',
23
+ allowSelfSignedCerts: false,
24
+ };
25
+ let credentials = [];
26
+ let apiMessages = [];
27
+ let historyKey = 'NexoWatt.EOS.Assist.anonymous';
28
+ let loading = false;
15
29
 
16
30
  const currentContext = () => {
17
31
  const hash = String(location.hash || '').toLowerCase();
18
- if (hash.includes('tab-adapters')) return ['Module', 'Module suchen, Version prüfen, Instanz konfigurieren und Datenpunkte kontrollieren.'];
19
- if (hash.includes('tab-instances')) return ['Dienste', 'Status, Log und Abhängigkeiten prüfen, bevor ein Dienst neu gestartet wird.'];
20
- if (hash.includes('tab-objects')) return ['Datenpunkte', 'Read-DPs bleiben lesend. Write-DPs lassen sich typgerecht bedienen; sicherheitsrelevante Writes benötigen Admin/Service.'];
21
- if (hash.includes('tab-users')) return ['Zugänge & Rechte', 'Zugänge einer EOS-Rolle zuordnen und persönliche Passwörter sicher zurücksetzen.'];
22
- if (hash.includes('tab-intro')) return ['Übersicht', 'Anlagenstatus, Module, Dienste und die für deine Rolle freigegebenen Bereiche im Blick behalten.'];
23
- return ['EOS Assist', 'NexoWatt EOS unterstützt dich beim Einrichten, Prüfen und Absichern des Systems.'];
32
+ if (hash.includes('tab-adapters')) {
33
+ return ['Module', 'Fragen zu installierten Modulen, Versionen, Kompatibilität und Einrichtung.'];
34
+ }
35
+ if (hash.includes('tab-instances')) {
36
+ return ['Dienste', 'Fragen zu laufenden Diensten, Status, Einstellungen, Speicherverbrauch und Fehlern.'];
37
+ }
38
+ if (hash.includes('tab-objects')) {
39
+ return ['Datenpunkte', 'Fragen zu Messwerten, Metadaten, Schreibfreigaben und aktuellen Zuständen.'];
40
+ }
41
+ if (hash.includes('tab-enums')) {
42
+ return ['Struktur', 'Fragen zu Räumen, Funktionen, Smart-Home-Zuordnungen und Geräten.'];
43
+ }
44
+ if (hash.includes('tab-logs')) {
45
+ return ['Systemlogs', 'Fragen zu aktuellen Meldungen, Verbindungen und Fehlerursachen.'];
46
+ }
47
+ if (hash.includes('tab-users')) {
48
+ return ['Zugänge & Rechte', 'Fragen zu EOS-Rollen, Zugängen und erlaubten Funktionen.'];
49
+ }
50
+ if (hash.includes('nexowatt-backup') || hash.includes('tab-backup')) {
51
+ return ['NexoWatt Backup', 'Fragen zu Sicherungsstatus, Sicherungszielen und Wiederherstellung.'];
52
+ }
53
+ if (hash.includes('tab-intro')) {
54
+ return ['Übersicht', 'Fragen zum Anlagenstatus und zu allen freigegebenen EOS-Bereichen.'];
55
+ }
56
+ return ['EOS Assist', 'Fragen zum laufenden System, Einstellungen, Modulen, Geräten und Datenpunkten.'];
24
57
  };
25
58
 
26
- const answer = query => {
27
- const text = String(query || '').toLowerCase();
28
- if (/datenpunkt|write|schreib|wert|state/.test(text)) return 'Prüfe Schreibfreigabe, Datentyp und EOS-Rolle. Boolean und Trigger werden direkt bedient; Zahlen, Texte und Auswahlwerte öffnen den nativen Wertdialog.';
29
- if (/update|aktualis/.test(text)) return 'Warte, bis Module und Verbindung vollständig geladen sind. Danach das Update starten und den Befehlsdialog bis zum Abschluss geöffnet lassen.';
30
- if (/wallbox|ocpp|lade/.test(text)) return 'Für Ladepunkte zuerst Status und Frische der OCPP-Daten prüfen. Schreibbefehle nur über die vorgesehenen Steuerdatenpunkte senden.';
31
- if (/rechte|benutzer|rolle|passwort/.test(text)) return 'Admin/Service besitzt Vollzugriff. Installateur erhält Inbetriebnahme und Fehlersuche sowie Endkunden-Reset. Endkunde erhält freigegebene Bedien- und Smart-Home-Funktionen.';
32
- if (/backup|sicherung/.test(text)) return 'Für den normalen Betrieb die NexoWatt Sicherung verwenden. Die interne System-Notfallsicherung ist ausschließlich für Admin/Service sichtbar.';
33
- return 'Beschreibe Gerät, Datenpunkt oder Fehlerbild genauer. EOS Assist grenzt danach Oberfläche, Modul, Protokoll oder Gerätekommunikation ein.';
59
+ const findToolbar = () =>
60
+ document.querySelector(
61
+ '.eos-top-toolbar, #root > .MuiPaper-root > .MuiAppBar-root .MuiToolbar-root, .MuiAppBar-root .MuiToolbar-root',
62
+ );
63
+ const findUserAnchor = toolbar =>
64
+ Array.from(toolbar?.children || []).find(
65
+ child => child.querySelector?.('.MuiAvatar-root') || /\b(admin|installer|guest|endkunde|service)\b/i.test(child.textContent || ''),
66
+ ) || null;
67
+
68
+ const socketPath = () => {
69
+ let parts = location.pathname.split('/');
70
+ parts.pop();
71
+ if (location.pathname.match(/^\/admin\//)) parts = [];
72
+ const value = `${parts.join('/')}/socket.io`.replace(/\/{2,}/g, '/');
73
+ return value.startsWith('/') ? value : `/${value}`;
34
74
  };
35
75
 
36
- const findToolbar = () => document.querySelector('.eos-top-toolbar, #root > .MuiPaper-root > .MuiAppBar-root .MuiToolbar-root, .MuiAppBar-root .MuiToolbar-root');
37
- const findUserAnchor = toolbar => Array.from(toolbar?.children || []).find(child => child.querySelector?.('.MuiAvatar-root') || /\b(admin|installer|guest|endkunde|service)\b/i.test(child.textContent || '')) || null;
76
+ const emit = (event, payload = {}, timeoutMs = 30_000) =>
77
+ new Promise((resolve, reject) => {
78
+ if (!socket?.connected) {
79
+ reject(new Error('Keine Verbindung zum EOS-System.'));
80
+ return;
81
+ }
82
+ let finished = false;
83
+ const timer = window.setTimeout(() => {
84
+ if (finished) return;
85
+ finished = true;
86
+ reject(new Error('Zeitüberschreitung bei der EOS-Anfrage.'));
87
+ }, timeoutMs);
88
+ socket.emit(event, payload, response => {
89
+ if (finished) return;
90
+ finished = true;
91
+ window.clearTimeout(timer);
92
+ resolve(response || {});
93
+ });
94
+ });
38
95
 
39
96
  const remove = () => document.getElementById('eos-assist-root')?.remove();
97
+ const escapeRole = role => (role === 'admin' ? 'Admin / Service' : role === 'installer' ? 'Installateur' : 'Endkunde');
98
+
99
+ const persistHistory = () => {
100
+ try {
101
+ sessionStorage.setItem(historyKey, JSON.stringify({ apiMessages: apiMessages.slice(-24) }));
102
+ } catch {
103
+ // A conversation must still work if session storage is unavailable.
104
+ }
105
+ };
106
+
107
+ const loadHistory = () => {
108
+ apiMessages = [];
109
+ try {
110
+ const parsed = JSON.parse(sessionStorage.getItem(historyKey) || '{}');
111
+ if (Array.isArray(parsed.apiMessages)) {
112
+ apiMessages = parsed.apiMessages
113
+ .filter(
114
+ message =>
115
+ (message?.role === 'user' || message?.role === 'assistant') &&
116
+ typeof message.content === 'string',
117
+ )
118
+ .slice(-24);
119
+ }
120
+ } catch {
121
+ apiMessages = [];
122
+ }
123
+ };
124
+
125
+ const appendLinkAwareText = (container, text) => {
126
+ const source = String(text || '');
127
+ const regex = /\[([^\]]+)\]\((#[^)]+)\)/g;
128
+ let cursor = 0;
129
+ let match;
130
+ while ((match = regex.exec(source))) {
131
+ if (match.index > cursor) container.append(document.createTextNode(source.substring(cursor, match.index)));
132
+ const link = document.createElement('a');
133
+ link.href = match[2];
134
+ link.textContent = match[1];
135
+ link.addEventListener(
136
+ 'click',
137
+ event => {
138
+ event.preventDefault();
139
+ location.hash = match[2];
140
+ },
141
+ { signal: abort.signal },
142
+ );
143
+ container.append(link);
144
+ cursor = regex.lastIndex;
145
+ }
146
+ if (cursor < source.length) container.append(document.createTextNode(source.substring(cursor)));
147
+ };
148
+
149
+ const appendMessage = (role, text, extraClass = '') => {
150
+ const root = document.getElementById('eos-assist-root');
151
+ const transcript = root?.querySelector('.eos-assist-transcript');
152
+ if (!transcript) return;
153
+ transcript.querySelector('.eos-assist-welcome')?.remove();
154
+ const article = document.createElement('article');
155
+ article.className = `eos-assist-message eos-assist-message-${role}${extraClass ? ` ${extraClass}` : ''}`;
156
+ const label = document.createElement('strong');
157
+ label.textContent = role === 'user' ? 'Du' : role === 'error' ? 'Hinweis' : 'EOS Assist';
158
+ const body = document.createElement('div');
159
+ appendLinkAwareText(body, text);
160
+ article.append(label, body);
161
+ transcript.append(article);
162
+ transcript.scrollTop = transcript.scrollHeight;
163
+ };
164
+
165
+ const renderHistory = () => {
166
+ const root = document.getElementById('eos-assist-root');
167
+ const transcript = root?.querySelector('.eos-assist-transcript');
168
+ if (!transcript) return;
169
+ transcript.innerHTML = '';
170
+ if (!apiMessages.length) {
171
+ const welcome = document.createElement('div');
172
+ welcome.className = 'eos-assist-welcome';
173
+ welcome.textContent =
174
+ 'Frage nach dem aktuellen Anlagenstatus, einer Einstellung, einem Modul, Dienst, Gerät, Datenpunkt oder Fehler.';
175
+ transcript.append(welcome);
176
+ return;
177
+ }
178
+ for (const message of apiMessages) appendMessage(message.role, message.content);
179
+ };
180
+
181
+ const setConnectionText = (text, kind = '') => {
182
+ const element = document.querySelector('#eos-assist-root .eos-assist-status');
183
+ if (!element) return;
184
+ element.textContent = text;
185
+ element.dataset.kind = kind;
186
+ };
187
+
188
+ const updateStatusUi = () => {
189
+ const root = document.getElementById('eos-assist-root');
190
+ if (!root) return;
191
+ const configure = root.querySelector('.eos-assist-configure');
192
+ configure?.classList.toggle('eos-assist-config-hidden', !status?.canConfigure);
193
+ root.classList.toggle('eos-assist-not-configured', status?.configured === false);
194
+ const role = status?.role ? escapeRole(status.role) : '';
195
+ if (!socket?.connected) setConnectionText('Verbindung wird hergestellt …', 'waiting');
196
+ else if (!status) setConnectionText('Systemstatus wird geladen …', 'waiting');
197
+ else if (!status.configured) {
198
+ setConnectionText(
199
+ status.canConfigure
200
+ ? 'EOS Assist muss einmalig konfiguriert werden.'
201
+ : 'EOS Assist wurde noch nicht durch Admin / Service konfiguriert.',
202
+ 'warning',
203
+ );
204
+ } else setConnectionText(`Verbunden · ${role} · schreibgeschützt`, 'ready');
205
+ };
206
+
207
+ const fillSettingsForm = () => {
208
+ const root = document.getElementById('eos-assist-root');
209
+ if (!root) return;
210
+ const provider = root.querySelector('.eos-assist-provider');
211
+ const credential = root.querySelector('.eos-assist-credential');
212
+ const model = root.querySelector('.eos-assist-model');
213
+ const baseUrl = root.querySelector('.eos-assist-base-url');
214
+ const selfSigned = root.querySelector('.eos-assist-self-signed');
215
+ if (provider) provider.value = settings.provider || 'anthropic';
216
+ if (credential) {
217
+ credential.innerHTML = '<option value="">Kein Zugang ausgewählt</option>';
218
+ for (const entry of credentials) {
219
+ const option = document.createElement('option');
220
+ option.value = entry.id || '';
221
+ option.textContent = entry.name || entry.id || 'KI-Zugang';
222
+ credential.append(option);
223
+ }
224
+ credential.value = settings.credentialId || '';
225
+ }
226
+ if (model) model.value = settings.model || '';
227
+ if (baseUrl) baseUrl.value = settings.baseUrl || '';
228
+ if (selfSigned) selfSigned.checked = settings.allowSelfSignedCerts === true;
229
+ root.classList.toggle('eos-assist-custom-provider', provider?.value === 'custom');
230
+ };
231
+
232
+ const readSettingsForm = () => {
233
+ const root = document.getElementById('eos-assist-root');
234
+ return {
235
+ provider: root?.querySelector('.eos-assist-provider')?.value || 'anthropic',
236
+ credentialId: root?.querySelector('.eos-assist-credential')?.value || '',
237
+ model: root?.querySelector('.eos-assist-model')?.value?.trim() || '',
238
+ baseUrl: root?.querySelector('.eos-assist-base-url')?.value?.trim() || '',
239
+ allowSelfSignedCerts: root?.querySelector('.eos-assist-self-signed')?.checked === true,
240
+ };
241
+ };
242
+
243
+ const loadStatus = async () => {
244
+ try {
245
+ const response = await emit('eosAssistStatus');
246
+ if (response.error) throw new Error(response.error);
247
+ status = response;
248
+ settings = { ...settings, ...(response.settings || {}) };
249
+ credentials = Array.isArray(response.credentials) ? response.credentials : [];
250
+ const nextKey = `NexoWatt.EOS.Assist.${response.user || 'anonymous'}`;
251
+ if (historyKey !== nextKey) {
252
+ historyKey = nextKey;
253
+ loadHistory();
254
+ renderHistory();
255
+ }
256
+ fillSettingsForm();
257
+ updateStatusUi();
258
+ } catch (error) {
259
+ status = null;
260
+ setConnectionText(error instanceof Error ? error.message : String(error), 'error');
261
+ }
262
+ };
263
+
264
+ const connectSocket = () => {
265
+ if (destroyed || socket?.connected) return;
266
+ if (!window.io?.connect) {
267
+ reconnectTimer = window.setTimeout(connectSocket, 250);
268
+ return;
269
+ }
270
+ socket = window.io.connect('/', { path: socketPath() });
271
+ socket.on('connect', () => {
272
+ setConnectionText('Verbunden. Systemstatus wird geladen …', 'waiting');
273
+ void loadStatus();
274
+ });
275
+ socket.on('disconnect', () => {
276
+ status = null;
277
+ updateStatusUi();
278
+ });
279
+ socket.on('reauthenticate', () => {
280
+ setConnectionText('Die Sitzung ist abgelaufen. Bitte erneut anmelden.', 'error');
281
+ });
282
+ socket.on('connect_error', error => {
283
+ setConnectionText(`Verbindungsfehler: ${error?.message || error}`, 'error');
284
+ });
285
+ };
286
+
287
+ const setBusy = busy => {
288
+ loading = busy;
289
+ const root = document.getElementById('eos-assist-root');
290
+ const input = root?.querySelector('.eos-assist-input');
291
+ const send = root?.querySelector('.eos-assist-send');
292
+ if (input) input.disabled = busy;
293
+ if (send) {
294
+ send.disabled = busy;
295
+ send.textContent = busy ? 'Prüft …' : 'Fragen';
296
+ }
297
+ };
298
+
299
+ const sendQuestion = async () => {
300
+ const root = document.getElementById('eos-assist-root');
301
+ const input = root?.querySelector('.eos-assist-input');
302
+ const text = String(input?.value || '').trim();
303
+ if (!text || loading) return;
304
+ if (!status?.configured) {
305
+ appendMessage(
306
+ 'error',
307
+ status?.canConfigure
308
+ ? 'Öffne die Einstellungen von EOS Assist und hinterlege Anbieter, Zugang und Modell.'
309
+ : 'EOS Assist muss zuerst durch Admin / NexoWatt Service konfiguriert werden.',
310
+ );
311
+ return;
312
+ }
313
+ apiMessages.push({ role: 'user', content: text });
314
+ apiMessages = apiMessages.slice(-24);
315
+ persistHistory();
316
+ appendMessage('user', text);
317
+ input.value = '';
318
+ setBusy(true);
319
+ setConnectionText('EOS Assist prüft das aktuelle System und die freigegebenen Daten …', 'waiting');
320
+ try {
321
+ const response = await emit(
322
+ 'eosAssist',
323
+ { messages: apiMessages, uiContext: { hash: location.hash } },
324
+ 620_000,
325
+ );
326
+ if (response.error) {
327
+ appendMessage('error', response.error);
328
+ if (response.code === 'notConfigured') {
329
+ status = { ...(status || {}), configured: false, canConfigure: response.canConfigure === true };
330
+ }
331
+ updateStatusUi();
332
+ return;
333
+ }
334
+ const answer = String(response.content || 'Es wurde keine Antwort erzeugt.');
335
+ apiMessages.push({ role: 'assistant', content: answer });
336
+ apiMessages = apiMessages.slice(-24);
337
+ persistHistory();
338
+ appendMessage('assistant', answer);
339
+ for (const action of response.clientActions || []) {
340
+ if (action?.type === 'navigate' && typeof action.hash === 'string') {
341
+ location.hash = action.hash.startsWith('#') ? action.hash : `#${action.hash}`;
342
+ }
343
+ }
344
+ setConnectionText(`Antwort auf Basis des aktuellen EOS-Systems · ${escapeRole(status.role)}`, 'ready');
345
+ } catch (error) {
346
+ appendMessage('error', error instanceof Error ? error.message : String(error));
347
+ setConnectionText('Die Anfrage konnte nicht abgeschlossen werden.', 'error');
348
+ } finally {
349
+ setBusy(false);
350
+ input?.focus();
351
+ }
352
+ };
353
+
354
+ const openSettings = open => {
355
+ const root = document.getElementById('eos-assist-root');
356
+ if (!root || !status?.canConfigure) return;
357
+ root.classList.toggle('eos-assist-settings-open', open);
358
+ fillSettingsForm();
359
+ };
360
+
361
+ const testSettings = async () => {
362
+ const payload = readSettingsForm();
363
+ setConnectionText('Verbindung und verfügbare Modelle werden geprüft …', 'waiting');
364
+ try {
365
+ const response = await emit('eosAssistTestSettings', payload, 30_000);
366
+ if (response.error) throw new Error(response.error);
367
+ const models = Array.isArray(response.models) ? response.models : [];
368
+ const list = document.querySelector('#eos-assist-root .eos-assist-model-list');
369
+ if (list) {
370
+ list.innerHTML = '';
371
+ for (const id of models) {
372
+ const option = document.createElement('option');
373
+ option.value = id;
374
+ list.append(option);
375
+ }
376
+ }
377
+ const model = document.querySelector('#eos-assist-root .eos-assist-model');
378
+ if (model && !model.value && models[0]) model.value = models[0];
379
+ setConnectionText(`${models.length} Modell(e) gefunden.`, 'ready');
380
+ } catch (error) {
381
+ setConnectionText(error instanceof Error ? error.message : String(error), 'error');
382
+ }
383
+ };
384
+
385
+ const saveSettings = async () => {
386
+ const payload = readSettingsForm();
387
+ setConnectionText('EOS-Assist-Einstellungen werden gespeichert …', 'waiting');
388
+ try {
389
+ const response = await emit('eosAssistSaveSettings', payload, 30_000);
390
+ if (response.error) throw new Error(response.error);
391
+ settings = { ...settings, ...(response.settings || payload) };
392
+ status = { ...(status || {}), configured: response.configured === true };
393
+ openSettings(false);
394
+ updateStatusUi();
395
+ setConnectionText('EOS Assist ist konfiguriert und schreibgeschützt bereit.', 'ready');
396
+ } catch (error) {
397
+ setConnectionText(error instanceof Error ? error.message : String(error), 'error');
398
+ }
399
+ };
400
+
401
+ const newConversation = () => {
402
+ apiMessages = [];
403
+ persistHistory();
404
+ renderHistory();
405
+ document.querySelector('#eos-assist-root .eos-assist-input')?.focus();
406
+ };
407
+
40
408
  const ensure = () => {
41
409
  if (destroyed) return;
42
410
  const hasApp = !!document.getElementById('app-paper');
@@ -54,40 +422,101 @@
54
422
  root.className = 'eos-assist-root eos-assist-header-root';
55
423
  root.innerHTML = `
56
424
  <button class="eos-assist-button" type="button" aria-expanded="false" title="EOS Assist">
57
- <span class="eos-assist-dot"></span><strong>EOS Assist</strong><small>Hilfe</small>
425
+ <span class="eos-assist-dot"></span><strong>EOS Assist</strong><small>Systemhilfe</small>
58
426
  </button>
59
427
  <div class="eos-assist-panel" role="dialog" aria-label="EOS Assist">
60
- <div class="eos-assist-head"><img src="${logoUrl}" alt="NexoWatt EOS"><div><strong class="eos-assist-title"></strong><span class="eos-assist-text"></span></div><button class="eos-assist-close" type="button" aria-label="Schließen">×</button></div>
61
- <div class="eos-assist-input-row"><input class="eos-assist-input" placeholder="Frage zu EOS, Modul oder Datenpunkt"><button class="eos-assist-send" type="button">Fragen</button></div>
62
- <div class="eos-assist-answer">Beschreibe kurz, was geprüft oder eingerichtet werden soll.</div>
428
+ <div class="eos-assist-head">
429
+ <img src="${logoUrl}" alt="NexoWatt EOS">
430
+ <div><strong class="eos-assist-title">EOS Assist</strong><span class="eos-assist-text"></span></div>
431
+ <button class="eos-assist-close" type="button" aria-label="Schließen">×</button>
432
+ </div>
433
+ <div class="eos-assist-toolbar">
434
+ <button class="eos-assist-new" type="button">Neue Unterhaltung</button>
435
+ <button class="eos-assist-configure eos-assist-config-hidden" type="button">Einstellungen</button>
436
+ </div>
437
+ <div class="eos-assist-conversation">
438
+ <div class="eos-assist-transcript" aria-live="polite"></div>
439
+ <div class="eos-assist-input-row">
440
+ <textarea class="eos-assist-input" rows="2" maxlength="4000" placeholder="Frage zum System, einer Einstellung, einem Modul oder Datenpunkt"></textarea>
441
+ <button class="eos-assist-send" type="button">Fragen</button>
442
+ </div>
443
+ </div>
444
+ <div class="eos-assist-settings" aria-label="EOS Assist Einstellungen">
445
+ <label>Anbieter<select class="eos-assist-provider">
446
+ <option value="anthropic">Anthropic</option>
447
+ <option value="openai">OpenAI</option>
448
+ <option value="gemini">Gemini</option>
449
+ <option value="deepseek">DeepSeek</option>
450
+ <option value="custom">Lokal / individueller Anbieter</option>
451
+ </select></label>
452
+ <label>Gespeicherter KI-Zugang<select class="eos-assist-credential"></select></label>
453
+ <label class="eos-assist-base-url-label">Basis-URL<input class="eos-assist-base-url" placeholder="http://127.0.0.1:11434/v1"></label>
454
+ <label>Modell<input class="eos-assist-model" list="eos-assist-model-list" placeholder="Modellbezeichnung"><datalist id="eos-assist-model-list" class="eos-assist-model-list"></datalist></label>
455
+ <label class="eos-assist-check"><input class="eos-assist-self-signed" type="checkbox"> Selbstsigniertes Zertifikat zulassen</label>
456
+ <div class="eos-assist-settings-actions">
457
+ <button class="eos-assist-settings-back" type="button">Zurück</button>
458
+ <button class="eos-assist-test" type="button">Verbindung testen</button>
459
+ <button class="eos-assist-save" type="button">Speichern</button>
460
+ </div>
461
+ </div>
462
+ <div class="eos-assist-status" data-kind="waiting">Verbindung wird hergestellt …</div>
63
463
  </div>`;
64
464
  const userAnchor = findUserAnchor(toolbar);
65
465
  toolbar.insertBefore(root, userAnchor);
66
- root.addEventListener('click', event => {
67
- const button = event.target?.closest?.('button');
68
- if (!button) return;
69
- if (button.classList.contains('eos-assist-button')) {
70
- const open = !root.classList.contains('eos-assist-open');
71
- root.classList.toggle('eos-assist-open', open);
72
- button.setAttribute('aria-expanded', open ? 'true' : 'false');
73
- if (open) setTimeout(() => root.querySelector('.eos-assist-input')?.focus(), 30);
74
- } else if (button.classList.contains('eos-assist-close')) {
75
- root.classList.remove('eos-assist-open');
76
- root.querySelector('.eos-assist-button')?.setAttribute('aria-expanded', 'false');
77
- } else if (button.classList.contains('eos-assist-send')) {
78
- const input = root.querySelector('.eos-assist-input');
79
- root.querySelector('.eos-assist-answer').textContent = answer(input?.value || '');
80
- }
81
- }, { signal: abort.signal });
82
- root.addEventListener('keydown', event => {
83
- if (event.key === 'Enter' && event.target?.classList?.contains('eos-assist-input')) {
84
- root.querySelector('.eos-assist-answer').textContent = answer(event.target.value || '');
85
- }
86
- if (event.key === 'Escape') {
87
- root.classList.remove('eos-assist-open');
88
- root.querySelector('.eos-assist-button')?.setAttribute('aria-expanded', 'false');
89
- }
90
- }, { signal: abort.signal });
466
+ renderHistory();
467
+ root.addEventListener(
468
+ 'click',
469
+ event => {
470
+ const button = event.target?.closest?.('button');
471
+ if (!button) return;
472
+ if (button.classList.contains('eos-assist-button')) {
473
+ const open = !root.classList.contains('eos-assist-open');
474
+ root.classList.toggle('eos-assist-open', open);
475
+ button.setAttribute('aria-expanded', open ? 'true' : 'false');
476
+ if (open) {
477
+ void loadStatus();
478
+ window.setTimeout(() => root.querySelector('.eos-assist-input')?.focus(), 30);
479
+ }
480
+ } else if (button.classList.contains('eos-assist-close')) {
481
+ root.classList.remove('eos-assist-open', 'eos-assist-settings-open');
482
+ root.querySelector('.eos-assist-button')?.setAttribute('aria-expanded', 'false');
483
+ } else if (button.classList.contains('eos-assist-send')) void sendQuestion();
484
+ else if (button.classList.contains('eos-assist-new')) newConversation();
485
+ else if (button.classList.contains('eos-assist-configure')) openSettings(true);
486
+ else if (button.classList.contains('eos-assist-settings-back')) openSettings(false);
487
+ else if (button.classList.contains('eos-assist-test')) void testSettings();
488
+ else if (button.classList.contains('eos-assist-save')) void saveSettings();
489
+ },
490
+ { signal: abort.signal },
491
+ );
492
+ root.addEventListener(
493
+ 'keydown',
494
+ event => {
495
+ if (
496
+ event.key === 'Enter' &&
497
+ !event.shiftKey &&
498
+ event.target?.classList?.contains('eos-assist-input')
499
+ ) {
500
+ event.preventDefault();
501
+ void sendQuestion();
502
+ }
503
+ if (event.key === 'Escape') {
504
+ if (root.classList.contains('eos-assist-settings-open')) openSettings(false);
505
+ else {
506
+ root.classList.remove('eos-assist-open');
507
+ root.querySelector('.eos-assist-button')?.setAttribute('aria-expanded', 'false');
508
+ }
509
+ }
510
+ },
511
+ { signal: abort.signal },
512
+ );
513
+ root.querySelector('.eos-assist-provider')?.addEventListener(
514
+ 'change',
515
+ event => root.classList.toggle('eos-assist-custom-provider', event.target.value === 'custom'),
516
+ { signal: abort.signal },
517
+ );
518
+ updateStatusUi();
519
+ connectSocket();
91
520
  } else if (root.parentElement !== toolbar) {
92
521
  const userAnchor = findUserAnchor(toolbar);
93
522
  toolbar.insertBefore(root, userAnchor);
@@ -97,10 +526,10 @@
97
526
  root.querySelector('.eos-assist-text').textContent = text;
98
527
  };
99
528
 
100
- const connect = () => {
529
+ const connectDom = () => {
101
530
  const coordinator = window.NEXOWATT_EOS_DOM_COORDINATOR;
102
531
  if (!coordinator?.subscribe) {
103
- timer = setTimeout(connect, 250);
532
+ reconnectTimer = window.setTimeout(connectDom, 250);
104
533
  return;
105
534
  }
106
535
  unsubscribeDom = coordinator.subscribe(ensure);
@@ -108,10 +537,11 @@
108
537
 
109
538
  window.addEventListener('hashchange', ensure, { signal: abort.signal });
110
539
  window.addEventListener('resize', ensure, { signal: abort.signal });
111
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ensure, { once: true, signal: abort.signal });
112
- else ensure();
113
- connect();
114
- [250, 800, 1600].forEach(delay => setTimeout(ensure, delay));
540
+ if (document.readyState === 'loading') {
541
+ document.addEventListener('DOMContentLoaded', ensure, { once: true, signal: abort.signal });
542
+ } else ensure();
543
+ connectDom();
544
+ [250, 800, 1600].forEach(delay => window.setTimeout(ensure, delay));
115
545
 
116
546
  window.NEXOWATT_EOS_ASSIST = {
117
547
  version: VERSION,
@@ -120,7 +550,9 @@
120
550
  destroyed = true;
121
551
  abort.abort();
122
552
  unsubscribeDom?.();
123
- if (timer) clearTimeout(timer);
553
+ if (reconnectTimer) window.clearTimeout(reconnectTimer);
554
+ socket?.disconnect?.();
555
+ socket = null;
124
556
  remove();
125
557
  },
126
558
  };
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v89-basic-settings';
4
+ const VERSION = 'v93-basic-settings';
5
5
  const existing = window.NEXOWATT_EOS_BASIC_SETTINGS;
6
6
  if (existing?.version === VERSION) return;
7
7
  existing?.destroy?.();
@@ -1,6 +1,6 @@
1
1
  (() => {
2
2
  'use strict';
3
- const VERSION = 'v89-visible-branding';
3
+ const VERSION = 'v93-visible-branding-publisher';
4
4
  const existing = window.NEXOWATT_EOS_BRANDING_SANITIZER;
5
5
  if (existing?.version === VERSION) return;
6
6
  existing?.destroy?.();
@@ -8,6 +8,8 @@
8
8
  let unsubscribe = null;
9
9
  const skip = new Set(['SCRIPT','STYLE','CODE','PRE','TEXTAREA','NOSCRIPT']);
10
10
  const rewrite = value => String(value || '')
11
+ .replace(/\[\s*iobroker\.eos-admin@[^\]\r\n]+\]/gi, '[NexoWatt]')
12
+ .replace(/\[\s*eos-admin@[^\]\r\n]+\]/gi, '[NexoWatt]')
11
13
  .replace(/ioBroker\.admin/gi, 'NexoWatt EOS Admin')
12
14
  .replace(/ioBroker\s*Admin/gi, 'NexoWatt EOS Admin')
13
15
  .replace(/ioBroker/gi, 'NexoWatt EOS');
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v89-product-role-access';
4
+ const VERSION = 'v93-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 = 'v89-role-access';
4
+ const VERSION = 'v93-role-access';
5
5
  const existing = window.NEXOWATT_EOS_POLICY_CLIENT;
6
6
  if (existing?.version === VERSION) return;
7
7
  existing?.destroy?.();