iobroker.eos-admin 7.9.26 → 7.9.28

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.
@@ -2448,3 +2448,202 @@ html.eos-app .MuiAppBar-root .MuiToolbar-root img[alt*="user" i] {
2448
2448
  }
2449
2449
  }
2450
2450
 
2451
+
2452
+ /* === NexoWatt EOS v27: logo contrast, compact nav, EOS Assist ============
2453
+ Der Pfeil ist kein Voll-Schließen mehr: er schaltet nur zwischen normaler
2454
+ und kompakter Navigationsstufe um. Das Logo bekommt einen schwarzen Grund,
2455
+ damit das NexoWatt-Zeichen sauberer und hochwertiger wirkt. */
2456
+ html.eos-app .eos-brand-badge {
2457
+ grid-template-columns: 78px minmax(170px, 1fr) 12px !important;
2458
+ padding-left: 6px !important;
2459
+ background:
2460
+ radial-gradient(circle at 38px 29px, rgba(0, 255, 136, 0.18), transparent 44px),
2461
+ linear-gradient(135deg, rgba(4, 18, 26, 0.98), rgba(0, 7, 12, 0.84) 52%, rgba(0, 32, 26, 0.50)) !important;
2462
+ }
2463
+
2464
+ html.eos-app .eos-brand-badge-mark {
2465
+ background:
2466
+ radial-gradient(circle at 50% 44%, rgba(0, 255, 136, 0.16), rgba(0, 0, 0, 0.96) 58%, #000000 100%) !important;
2467
+ border-color: rgba(126, 255, 216, 0.84) !important;
2468
+ box-shadow:
2469
+ inset 0 0 0 1px rgba(255, 255, 255, 0.05),
2470
+ inset 0 0 20px rgba(0, 255, 136, 0.18),
2471
+ 0 0 0 1px rgba(0, 0, 0, 0.55),
2472
+ 0 0 20px rgba(0, 255, 136, 0.62),
2473
+ 0 0 40px rgba(0, 212, 255, 0.20) !important;
2474
+ transform: translateY(-5px) !important;
2475
+ }
2476
+
2477
+ html.eos-app .eos-brand-badge-logo {
2478
+ filter:
2479
+ brightness(2.35)
2480
+ contrast(1.22)
2481
+ saturate(1.48)
2482
+ drop-shadow(0 0 1px rgba(255, 255, 255, 0.95))
2483
+ drop-shadow(0 0 11px rgba(0, 255, 136, 0.82))
2484
+ drop-shadow(0 0 22px rgba(0, 212, 255, 0.28)) !important;
2485
+ }
2486
+
2487
+ /* Drawer/rail stays visible. The native arrow becomes a compact-mode toggle. */
2488
+ html.eos-app .MuiDrawer-paper,
2489
+ html.eos-app .eos-drawer {
2490
+ transform: none !important;
2491
+ visibility: visible !important;
2492
+ opacity: 1 !important;
2493
+ display: flex !important;
2494
+ padding-left: 86px !important;
2495
+ }
2496
+
2497
+ html.eos-app .eos-native-drawer-header {
2498
+ position: absolute !important;
2499
+ left: 12px !important;
2500
+ top: 8px !important;
2501
+ flex: 0 0 58px !important;
2502
+ width: 58px !important;
2503
+ min-width: 58px !important;
2504
+ max-width: 58px !important;
2505
+ height: 56px !important;
2506
+ min-height: 56px !important;
2507
+ margin: 0 !important;
2508
+ z-index: 4 !important;
2509
+ background: rgba(0, 0, 0, 0.38) !important;
2510
+ border-color: rgba(0, 255, 136, 0.38) !important;
2511
+ box-shadow: inset 0 0 16px rgba(0, 255, 136, 0.10), 0 0 20px rgba(0, 255, 136, 0.10) !important;
2512
+ }
2513
+
2514
+ html.eos-app .eos-native-drawer-header > div:first-child {
2515
+ width: 58px !important;
2516
+ min-width: 58px !important;
2517
+ max-width: 58px !important;
2518
+ padding: 4px 6px !important;
2519
+ }
2520
+
2521
+ html.eos-app .eos-native-drawer-header button.eos-nav-compact-toggle,
2522
+ html.eos-app .eos-native-drawer-header .eos-nav-compact-toggle {
2523
+ width: 46px !important;
2524
+ height: 46px !important;
2525
+ min-width: 46px !important;
2526
+ min-height: 46px !important;
2527
+ border-radius: 16px !important;
2528
+ background:
2529
+ radial-gradient(circle at 50% 50%, rgba(0, 255, 136, 0.18), rgba(0, 0, 0, 0.72)) !important;
2530
+ border: 1px solid rgba(0, 255, 136, 0.44) !important;
2531
+ box-shadow: 0 0 16px rgba(0, 255, 136, 0.18) !important;
2532
+ }
2533
+
2534
+ html.eos-app.eos-nav-compact .eos-native-drawer-header .eos-nav-compact-toggle svg,
2535
+ html.eos-app.eos-nav-compact .eos-native-drawer-header button.eos-nav-compact-toggle svg {
2536
+ transform: rotate(180deg) !important;
2537
+ }
2538
+
2539
+ html.eos-app .eos-scroll-nav,
2540
+ html.eos-app .MuiDrawer-paper .MuiList-root {
2541
+ margin-left: 0 !important;
2542
+ }
2543
+
2544
+ html.eos-app.eos-nav-compact .MuiDrawer-paper .MuiListItemButton-root,
2545
+ html.eos-app.eos-nav-compact .MuiDrawer-paper .MuiButtonBase-root.MuiListItemButton-root {
2546
+ width: 58px !important;
2547
+ min-width: 58px !important;
2548
+ max-width: 58px !important;
2549
+ padding: 0 !important;
2550
+ justify-content: center !important;
2551
+ gap: 0 !important;
2552
+ }
2553
+
2554
+ html.eos-app.eos-nav-compact .MuiDrawer-paper .MuiListItemText-root,
2555
+ html.eos-app.eos-nav-compact .MuiDrawer-paper .MuiListItemButton-root .MuiTypography-root,
2556
+ html.eos-app.eos-nav-compact .MuiDrawer-paper .MuiButtonBase-root.MuiListItemButton-root .MuiTypography-root {
2557
+ display: none !important;
2558
+ max-width: 0 !important;
2559
+ opacity: 0 !important;
2560
+ overflow: hidden !important;
2561
+ }
2562
+
2563
+ html.eos-app.eos-nav-compact .MuiDrawer-paper .MuiListItemIcon-root {
2564
+ min-width: 0 !important;
2565
+ margin: 0 !important;
2566
+ }
2567
+
2568
+ html.eos-app.eos-nav-compact .MuiDrawer-paper .MuiBadge-root .MuiBadge-badge {
2569
+ transform: scale(0.88) translate(42%, -42%) !important;
2570
+ }
2571
+
2572
+ /* EOS Assist: leichte Bedienhilfe als Vorbereitung für eine spätere echte KI. */
2573
+ .eos-assist-root {
2574
+ position: fixed !important;
2575
+ right: 26px !important;
2576
+ bottom: 24px !important;
2577
+ z-index: 1700 !important;
2578
+ font-family: inherit !important;
2579
+ }
2580
+
2581
+ .eos-assist-button {
2582
+ display: grid !important;
2583
+ grid-template-columns: 12px auto !important;
2584
+ grid-template-rows: auto auto !important;
2585
+ column-gap: 8px !important;
2586
+ align-items: center !important;
2587
+ min-width: 156px !important;
2588
+ padding: 10px 14px !important;
2589
+ border-radius: 18px !important;
2590
+ border: 1px solid rgba(0, 255, 136, 0.44) !important;
2591
+ background: linear-gradient(135deg, rgba(0, 255, 136, 0.19), rgba(0, 212, 255, 0.10) 62%, rgba(0, 0, 0, 0.64)) !important;
2592
+ color: #f3fffb !important;
2593
+ box-shadow: 0 14px 32px rgba(0, 0, 0, 0.38), 0 0 24px rgba(0, 255, 136, 0.18) !important;
2594
+ cursor: pointer !important;
2595
+ }
2596
+
2597
+ .eos-assist-button strong { grid-column: 2; font-weight: 900; font-size: 13px; line-height: 1; }
2598
+ .eos-assist-button small { grid-column: 2; color: rgba(226, 255, 247, .72); font-size: 10px; line-height: 1.1; }
2599
+ .eos-assist-dot { grid-row: 1 / span 2; width: 10px; height: 10px; border-radius: 999px; background: #16ff98; box-shadow: 0 0 12px rgba(0, 255, 136, .9); }
2600
+
2601
+ .eos-assist-panel {
2602
+ position: absolute !important;
2603
+ right: 0 !important;
2604
+ bottom: calc(100% + 12px) !important;
2605
+ width: min(420px, calc(100vw - 36px)) !important;
2606
+ padding: 16px !important;
2607
+ border-radius: 22px !important;
2608
+ border: 1px solid rgba(0, 255, 136, 0.32) !important;
2609
+ background:
2610
+ radial-gradient(circle at 18% 0%, rgba(0, 255, 136, .12), transparent 44%),
2611
+ linear-gradient(180deg, rgba(8, 29, 44, 0.98), rgba(2, 9, 20, 0.98)) !important;
2612
+ color: #f3fffb !important;
2613
+ box-shadow: 0 24px 60px rgba(0,0,0,.52), 0 0 26px rgba(0,255,136,.13) !important;
2614
+ opacity: 0 !important;
2615
+ pointer-events: none !important;
2616
+ transform: translateY(10px) scale(.98) !important;
2617
+ transition: opacity .16s ease, transform .16s ease !important;
2618
+ }
2619
+
2620
+ .eos-assist-root.eos-assist-open .eos-assist-panel {
2621
+ opacity: 1 !important;
2622
+ pointer-events: auto !important;
2623
+ transform: translateY(0) scale(1) !important;
2624
+ }
2625
+
2626
+ .eos-assist-head { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
2627
+ .eos-assist-head strong { display: block; font-size: 16px; font-weight: 950; margin-bottom: 5px; }
2628
+ .eos-assist-head span { display: block; color: rgba(226, 245, 255, .76); font-size: 12px; line-height: 1.35; }
2629
+ .eos-assist-close { width: 32px; height: 32px; border-radius: 12px; border: 1px solid rgba(0,255,136,.26); background: rgba(0,0,0,.34); color: #fff; cursor: pointer; }
2630
+ .eos-assist-steps { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 13px; }
2631
+ .eos-assist-steps span { padding: 5px 9px; border-radius: 999px; background: rgba(0,255,136,.10); border: 1px solid rgba(0,255,136,.18); color: rgba(237,255,250,.90); font-size: 11px; font-weight: 750; }
2632
+ .eos-assist-input-label { display: block; color: rgba(226,245,255,.78); font-size: 11px; margin: 0 0 6px; font-weight: 750; }
2633
+ .eos-assist-input-row { display: flex; gap: 8px; }
2634
+ .eos-assist-input { flex: 1; min-width: 0; border-radius: 14px; border: 1px solid rgba(98,190,255,.28); background: rgba(0,7,13,.62); color: #f3fffb; padding: 10px 12px; outline: none; }
2635
+ .eos-assist-send { border: 0; border-radius: 14px; padding: 0 13px; background: linear-gradient(135deg, #00ff88, #00d4ff); color: #001019; font-weight: 900; cursor: pointer; }
2636
+ .eos-assist-answer { margin-top: 12px; padding: 12px 13px; border-radius: 16px; border: 1px solid rgba(0,212,255,.20); background: rgba(0,0,0,.28); color: rgba(238,255,250,.90); font-size: 12px; line-height: 1.42; }
2637
+
2638
+ @media (max-width: 720px) {
2639
+ html.eos-app .MuiDrawer-paper,
2640
+ html.eos-app .eos-drawer { padding-left: 68px !important; }
2641
+ html.eos-app .eos-native-drawer-header { left: 8px !important; width: 50px !important; min-width: 50px !important; max-width: 50px !important; }
2642
+ html.eos-app .eos-native-drawer-header > div:first-child { width: 50px !important; min-width: 50px !important; max-width: 50px !important; }
2643
+ html.eos-app .eos-native-drawer-header button.eos-nav-compact-toggle,
2644
+ html.eos-app .eos-native-drawer-header .eos-nav-compact-toggle { width: 40px !important; height: 40px !important; min-width: 40px !important; min-height: 40px !important; }
2645
+ .eos-assist-root { right: 14px !important; bottom: 16px !important; }
2646
+ .eos-assist-button { min-width: 54px !important; width: 54px !important; height: 54px !important; border-radius: 18px !important; grid-template-columns: 1fr !important; padding: 0 !important; place-items: center !important; }
2647
+ .eos-assist-button strong, .eos-assist-button small { display: none !important; }
2648
+ .eos-assist-dot { grid-column: 1; grid-row: 1; width: 14px; height: 14px; }
2649
+ }
@@ -31,7 +31,7 @@
31
31
  rel="stylesheet"
32
32
  href="css/leaflet.css"
33
33
  />
34
- <link rel="stylesheet" href="./css/eos-branding.css?v=26" />
34
+ <link rel="stylesheet" href="./css/eos-branding.css?v=28" />
35
35
  <link
36
36
  rel="manifest"
37
37
  href="manifest.json"
@@ -154,8 +154,9 @@
154
154
  <script type="module" crossorigin src="./assets/index-CQZugZ1z.js"></script>
155
155
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js">
156
156
  <link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js">
157
- <script defer src="./js/eos-branding.js?v=26"></script>
158
- <script defer src="./js/eos-security-ui.js?v=26"></script>
157
+ <script defer src="./js/eos-branding.js?v=28"></script>
158
+ <script defer src="./js/eos-security-ui.js?v=28"></script>
159
+ <script defer src="./js/eos-assistant.js?v=28"></script>
159
160
  </head>
160
161
  <body>
161
162
  <noscript>You need to enable JavaScript to run this app.</noscript>
@@ -0,0 +1,203 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ window.NEXOWATT_EOS_ASSIST_VERSION = 'v27-local-setup-guide';
5
+
6
+ const ASSET_BASE = (() => {
7
+ const script = document.currentScript?.src || document.querySelector('script[src*="eos-assistant.js"]')?.src || window.location.href;
8
+ return new URL('../', script).href;
9
+ })();
10
+ const asset = path => new URL(path.replace(/^\.\//, ''), ASSET_BASE).href;
11
+ const LOGO = asset('img/eos/nexowatt-192.png');
12
+ const STORE_KEY = 'nexowatt.eos.assist.open';
13
+ const state = { checked: false, allowed: true };
14
+
15
+ const fetchAssistantPolicy = async () => {
16
+ if (state.checked) return state.allowed;
17
+ state.checked = true;
18
+ try {
19
+ const url = new URL('eos/security/status', ASSET_BASE).href;
20
+ const response = await fetch(url, { credentials: 'same-origin', cache: 'no-store' });
21
+ if (!response.ok) return state.allowed;
22
+ const json = await response.json();
23
+ const assistant = json.assistant || {};
24
+ const enabled = assistant.enabled !== false && json.eosAssistantEnabled !== false;
25
+ const adminOnly = assistant.adminOnly === true || json.eosAssistantAdminOnly === true;
26
+ const isAdmin = json.isAdmin === true || json.isAdministrator === true || json.isEosAdminGroup === true;
27
+ state.allowed = enabled && (!adminOnly || isAdmin);
28
+ } catch {
29
+ state.allowed = true;
30
+ }
31
+ return state.allowed;
32
+ };
33
+
34
+ const normalize = value => String(value || '')
35
+ .toLowerCase()
36
+ .normalize('NFD')
37
+ .replace(/[\u0300-\u036f]/g, '')
38
+ .replace(/\s+/g, ' ')
39
+ .trim();
40
+
41
+ const route = () => {
42
+ const hash = window.location.hash || '';
43
+ const text = normalize(document.body?.innerText || '');
44
+ if (hash.includes('tab-adapters') || /module|adapter/.test(text.slice(0, 4000))) return 'modules';
45
+ if (hash.includes('tab-instances') || /dienste|instanzen/.test(text.slice(0, 4000))) return 'services';
46
+ if (hash.includes('tab-logs') || /systemlogs|protokolle|log-grosse/.test(text.slice(0, 4000))) return 'logs';
47
+ if (hash.includes('tab-users') || /zugange & rechte|benutzer|rollen/.test(text.slice(0, 4000))) return 'rights';
48
+ if (hash.includes('tab-objects') || /datenpunkte|objekte/.test(text.slice(0, 4000))) return 'objects';
49
+ if (hash.includes('tab-hosts') || /system-hosts|hosts/.test(text.slice(0, 4000))) return 'hosts';
50
+ return 'overview';
51
+ };
52
+
53
+ const contextTitle = () => ({
54
+ modules: 'Module & Adapter',
55
+ services: 'Dienste & Instanzen',
56
+ logs: 'Systemlogs',
57
+ rights: 'Zugänge & Rechte',
58
+ objects: 'Datenpunkte',
59
+ hosts: 'System-Hosts',
60
+ overview: 'EOS Cockpit',
61
+ }[route()] || 'EOS Cockpit');
62
+
63
+ const routeAdvice = () => ({
64
+ modules: 'Hier installierst und aktualisierst du Module. Prüfe vor einer Installation immer: Zweck, benötigte Instanzen, Version, Abhängigkeiten und ob das Modul zu den geschützten EOS-Komponenten gehört.',
65
+ services: 'Hier siehst du laufende Dienste. Wichtig sind Status, Speicherverbrauch, Log-Level und ob eine Instanz automatisch startet. Fehlerhafte Dienste zuerst öffnen, Log prüfen, dann gezielt neu starten.',
66
+ logs: 'Hier erkennst du die Ursache vieler Probleme. Filtere nach Quelle und Level. Wiederholte Warnungen sind meist wichtiger als einzelne Meldungen.',
67
+ rights: 'Hier steuerst du Rollen und Benutzer. Installateur- und Endkundenrollen sollten keine geschützten EOS-Systemmodule löschen, stoppen oder aktivieren können.',
68
+ objects: 'Datenpunkte sind die technische Basis. Ändere Rollen, States und Alias-Strukturen nur bewusst, weil Adapter und Visualisierungen davon abhängen.',
69
+ hosts: 'Hier prüfst du Systemlast, Node.js, npm, Speicher und laufende Prozesse. Bei Updateproblemen zuerst Host-Status und freien Speicher prüfen.',
70
+ overview: 'Das Cockpit gibt den Überblick. Starte von hier aus mit Modulen, Diensten, Logs oder Rechten, je nachdem ob du einrichten, prüfen oder absichern möchtest.',
71
+ }[route()] || 'Wähle einen Bereich aus, dann kann EOS Assist dir die nächsten Schritte erklären.');
72
+
73
+ const answerFor = question => {
74
+ const q = normalize(question);
75
+ const r = route();
76
+ if (!q) return `Aktueller Bereich: ${contextTitle()}\n\n${routeAdvice()}\n\nDu kannst z. B. fragen: „Wie richte ich Modbus ein?“, „Warum ist ein Dienst rot?“ oder „Welche Rechte braucht der Installateur?“`;
77
+
78
+ if (/modbus|wechselrichter|tcp|rs485/.test(q)) {
79
+ return 'Modbus-Einrichtung:\n1. Modul installieren und Instanz anlegen.\n2. IP/Port oder seriellen Adapter prüfen.\n3. Unit-ID, Registerbereich und Byte-Reihenfolge dokumentieren.\n4. Dienst starten und Systemlogs auf Timeout/CRC prüfen.\n5. Erst wenn Werte stabil laufen, Datenpunkte in EOS Cockpit verwenden.';
80
+ }
81
+ if (/shelly|relay|schalter|steckdose/.test(q)) {
82
+ return 'Shelly-/Schaltmodul-Einrichtung:\n1. Gerät im gleichen Netzwerk erreichbar machen.\n2. Authentifizierung im Gerät setzen, damit keine Warnung wie „not protected via restricted login“ entsteht.\n3. Adapterinstanz konfigurieren und neu starten.\n4. In Datenpunkten prüfen, ob Power, Energy und Relay-State sauber aktualisieren.';
83
+ }
84
+ if (/ev|wallbox|laden|ladepunkt|ocpp/.test(q)) {
85
+ return 'Ladepunkt-/EVCS-Einrichtung:\n1. OCPP/Wallbox-Modul installieren.\n2. Ladepunkt-ID, Endpoint und Authentifizierung prüfen.\n3. Dienst starten und Verbindung im Log kontrollieren.\n4. Danach Ladeleistung, Fahrzeugstatus und Freigabe-Datenpunkte mit dem EOS Energiemanagement verknüpfen.';
86
+ }
87
+ if (/backup|backitup|sicherung|restore/.test(q)) {
88
+ return 'Backup-Empfehlung:\n1. BackItUp bleibt ein geschütztes EOS-Systemmodul.\n2. Vor Updates immer ein Backup starten.\n3. Restore-Ziel und Speicherort prüfen.\n4. Installateur-/Endkundenrollen sollten BackItUp nicht löschen oder deaktivieren dürfen.';
89
+ }
90
+ if (/recht|rolle|benutzer|installateur|kunde|admin/.test(q)) {
91
+ return 'Rechte-Empfehlung:\n1. Administrator: volle Wartung und Systemschutz.\n2. Installateur: Module konfigurieren, aber geschützte Adapter nicht löschen.\n3. Endkunde: Bedienung und Ansicht, keine Systemmodule.\n4. Geschützte Adapter legt der Administrator im EOS-Sicherheitsbereich fest.';
92
+ }
93
+ if (/log|fehler|warn|error|timeout|offline/.test(q)) {
94
+ return 'Fehleranalyse:\n1. In Systemlogs nach Quelle und Level filtern.\n2. Wiederholte Meldungen priorisieren.\n3. Bei Timeout Netzwerk/IP/Port prüfen.\n4. Bei Auth-Warnungen Zugangsdaten im Gerät und Adapter vergleichen.\n5. Danach nur die betroffene Instanz neu starten, nicht das komplette System.';
95
+ }
96
+ if (/update|aktualisieren|npm|repository|repo/.test(q)) {
97
+ return 'Update-Prüfung:\n1. NexoWatt-Repository aktiv setzen.\n2. Version im Repository und npm vergleichen.\n3. Bei EOS Admin: iobroker upgrade eos-admin, danach iobroker upload eos-admin.\n4. Browser mit Strg+F5 neu laden.\n5. Vor größeren Updates Backup erstellen.';
98
+ }
99
+ if (/dienst|instanz|start|stop|neustart/.test(q)) {
100
+ return 'Dienst prüfen:\n1. Status ansehen: grün/läuft, rot/gestoppt, gelb/Warnung.\n2. Speicherverbrauch und Log-Level prüfen.\n3. Bei Fehlern zuerst Log öffnen.\n4. Dann Instanz neu starten.\n5. Wenn der Fehler wiederkommt, Konfiguration und Abhängigkeiten prüfen.';
101
+ }
102
+ return `Ich habe deine Frage lokal eingeordnet.\n\nBereich: ${contextTitle()}\n\nEmpfohlene nächsten Schritte:\n1. Ziel klären: installieren, konfigurieren, Fehler suchen oder absichern.\n2. Betroffenes Modul/Dienst auswählen.\n3. Logs und Status prüfen.\n4. Änderung speichern und nur die betroffene Instanz neu starten.\n\nHinweis: Diese erste EOS-Assistenz arbeitet lokal ohne Cloud. Eine echte KI-Anbindung kann später über einen NexoWatt- oder MCP-Dienst ergänzt werden.`;
103
+ };
104
+
105
+ const setOpen = open => {
106
+ document.documentElement.classList.toggle('eos-assist-open', !!open);
107
+ try { localStorage.setItem(STORE_KEY, open ? '1' : '0'); } catch { /* ignore */ }
108
+ };
109
+
110
+ const renderAnswer = (panel, question) => {
111
+ const out = panel.querySelector('.eos-assist-answer');
112
+ if (out) out.textContent = answerFor(question);
113
+ const ctx = panel.querySelector('.eos-assist-context');
114
+ if (ctx) ctx.textContent = contextTitle();
115
+ const hint = panel.querySelector('.eos-assist-hint');
116
+ if (hint) hint.textContent = routeAdvice();
117
+ };
118
+
119
+ const ensureAssistant = () => {
120
+ if (!document.body || document.querySelector('.eos-assist-launcher')) return;
121
+ if (document.documentElement.classList.contains('eos-login')) return;
122
+ if (!document.getElementById('app-paper')) return;
123
+
124
+ const launcher = document.createElement('button');
125
+ launcher.type = 'button';
126
+ launcher.className = 'eos-assist-launcher';
127
+ launcher.innerHTML = '<span>✦</span><span>EOS Assist</span>';
128
+ launcher.setAttribute('aria-label', 'EOS Assist öffnen');
129
+
130
+ const panel = document.createElement('aside');
131
+ panel.className = 'eos-assist-panel';
132
+ panel.setAttribute('aria-label', 'EOS Assist');
133
+ panel.innerHTML = `
134
+ <div class="eos-assist-head">
135
+ <span class="eos-assist-logo"><img src="${LOGO}" alt="NexoWatt EOS" /></span>
136
+ <span><strong>EOS Assist</strong><small><span class="eos-assist-context"></span> · geführte Einrichtung</small></span>
137
+ <button type="button" class="eos-assist-close" aria-label="Schließen">×</button>
138
+ </div>
139
+ <div class="eos-assist-body">
140
+ <div class="eos-assist-card"><strong>Aktueller Hinweis</strong><p class="eos-assist-hint"></p></div>
141
+ <div class="eos-assist-actions">
142
+ <button type="button" data-question="Wie richte ich dieses Modul ein?">Modul einrichten</button>
143
+ <button type="button" data-question="Wie prüfe ich Fehler in den Logs?">Fehler prüfen</button>
144
+ <button type="button" data-question="Welche Rechte braucht der Installateur?">Rechte erklären</button>
145
+ <button type="button" data-question="Was muss ich vor einem Update beachten?">Update-Check</button>
146
+ </div>
147
+ <div class="eos-assist-input-row">
148
+ <input class="eos-assist-input" type="text" placeholder="Frage eingeben, z. B. Modbus einrichten" />
149
+ <button type="button" class="eos-assist-send">Fragen</button>
150
+ </div>
151
+ <div class="eos-assist-card eos-assist-answer"></div>
152
+ <div class="eos-assist-foot">Lokale Assistenz ohne Cloud. Für echte KI-Automation kann später ein NexoWatt-Dienst angebunden werden.</div>
153
+ </div>
154
+ `;
155
+
156
+ document.body.appendChild(launcher);
157
+ document.body.appendChild(panel);
158
+ renderAnswer(panel, '');
159
+
160
+ launcher.addEventListener('click', () => setOpen(!document.documentElement.classList.contains('eos-assist-open')));
161
+ panel.querySelector('.eos-assist-close')?.addEventListener('click', () => setOpen(false));
162
+ panel.querySelector('.eos-assist-send')?.addEventListener('click', () => renderAnswer(panel, panel.querySelector('.eos-assist-input')?.value || ''));
163
+ panel.querySelector('.eos-assist-input')?.addEventListener('keydown', event => {
164
+ if (event.key === 'Enter') renderAnswer(panel, event.currentTarget.value || '');
165
+ });
166
+ panel.querySelectorAll('button[data-question]').forEach(button => {
167
+ button.addEventListener('click', () => {
168
+ const question = button.getAttribute('data-question') || '';
169
+ const input = panel.querySelector('.eos-assist-input');
170
+ if (input) input.value = question;
171
+ renderAnswer(panel, question);
172
+ });
173
+ });
174
+
175
+ try { if (localStorage.getItem(STORE_KEY) === '1') setOpen(true); } catch { /* ignore */ }
176
+ };
177
+
178
+ const removeOnLogin = () => {
179
+ if (!document.documentElement.classList.contains('eos-login')) return;
180
+ document.querySelectorAll('.eos-assist-launcher,.eos-assist-panel').forEach(el => el.remove());
181
+ };
182
+
183
+ const run = () => {
184
+ removeOnLogin();
185
+ void fetchAssistantPolicy().then(allowed => {
186
+ if (!allowed) {
187
+ document.querySelectorAll('.eos-assist-launcher,.eos-assist-panel').forEach(el => el.remove());
188
+ return;
189
+ }
190
+ ensureAssistant();
191
+ const panel = document.querySelector('.eos-assist-panel');
192
+ if (panel) renderAnswer(panel, panel.querySelector('.eos-assist-input')?.value || '');
193
+ });
194
+ };
195
+
196
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run, { once: true });
197
+ else run();
198
+ window.addEventListener('hashchange', () => setTimeout(run, 80));
199
+ window.addEventListener('load', () => setTimeout(run, 120), { once: true });
200
+ new MutationObserver(() => {
201
+ if (!document.querySelector('.eos-assist-launcher')) run();
202
+ }).observe(document.documentElement, { childList: true, subtree: true });
203
+ })();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- window.NEXOWATT_EOS_UI_VERSION = 'v26-header-identity-polish';
4
+ window.NEXOWATT_EOS_UI_VERSION = 'v27-logo-nav-assist-polish';
5
5
 
6
6
  const BRAND = 'NexoWatt EOS';
7
7
  const EOS_MEANING = 'Energy Operation System';
@@ -72,6 +72,7 @@
72
72
  protectedAdapters: ['eos-admin', 'backitup'],
73
73
  },
74
74
  securityFetchStarted: false,
75
+ assistOpen: false,
75
76
  };
76
77
 
77
78
  const safe = fn => {
@@ -518,6 +519,26 @@
518
519
  }
519
520
  if (header) {
520
521
  header.classList.add('eos-native-drawer-header');
522
+ const toggleButton = header.querySelector('button, .MuiIconButton-root, [role="button"]');
523
+ if (toggleButton && !toggleButton.dataset.eosNavCompactToggle) {
524
+ toggleButton.dataset.eosNavCompactToggle = 'true';
525
+ toggleButton.classList.add('eos-nav-compact-toggle');
526
+ toggleButton.setAttribute('title', 'Navigation kompakt/normal umschalten');
527
+ toggleButton.setAttribute('aria-label', 'Navigation kompakt/normal umschalten');
528
+ const toggleCompact = event => {
529
+ event.preventDefault();
530
+ event.stopPropagation();
531
+ if (event.stopImmediatePropagation) event.stopImmediatePropagation();
532
+ const compact = !document.documentElement.classList.contains('eos-nav-compact');
533
+ document.documentElement.classList.toggle('eos-nav-compact', compact);
534
+ safe(() => localStorage.setItem('nexowatt:eosNavCompact', compact ? '1' : '0'));
535
+ toggleButton.setAttribute('aria-pressed', compact ? 'true' : 'false');
536
+ };
537
+ toggleButton.addEventListener('click', toggleCompact, true);
538
+ toggleButton.addEventListener('keydown', event => {
539
+ if (event.key === 'Enter' || event.key === ' ') toggleCompact(event);
540
+ }, true);
541
+ }
521
542
  const img = header.querySelector('img');
522
543
  if (img) patchImage(img);
523
544
  const avatarImg = header.querySelector('.MuiAvatar-img');
@@ -728,6 +749,116 @@
728
749
  });
729
750
  });
730
751
 
752
+
753
+ const applyNavCompactPreference = () => safe(() => {
754
+ const compact = localStorage.getItem('nexowatt:eosNavCompact') === '1';
755
+ document.documentElement.classList.toggle('eos-nav-compact', compact);
756
+ document.querySelectorAll('.eos-nav-compact-toggle').forEach(button => {
757
+ button.setAttribute('aria-pressed', compact ? 'true' : 'false');
758
+ button.setAttribute('title', compact ? 'Navigation normal anzeigen' : 'Navigation kompakt anzeigen');
759
+ });
760
+ });
761
+
762
+ const assistContext = () => {
763
+ const routes = routeInfo();
764
+ if (routes.adapters) return {
765
+ title: 'Module einrichten',
766
+ text: 'Wähle zuerst das passende Modul, prüfe Version und Instanzstatus und öffne dann die Konfiguration über die drei Punkte oder das Werkzeug-Symbol.',
767
+ steps: ['Modul suchen', 'Instanz anlegen', 'Verbindung testen', 'Datenpunkte prüfen']
768
+ };
769
+ if (routes.instances) return {
770
+ title: 'Dienste prüfen',
771
+ text: 'Kontrolliere Status, Port, Speicherverbrauch und Logmeldungen. Gestoppte Dienste erst nach Ursache und Abhängigkeiten prüfen.',
772
+ steps: ['Status ansehen', 'Log öffnen', 'Konfiguration prüfen', 'Dienst neu starten']
773
+ };
774
+ if (routes.users) return {
775
+ title: 'Zugänge & Rechte',
776
+ text: 'Installateure und Endkunden sollten nur die Module sehen, die sie wirklich bedienen dürfen. Geschützte EOS-Module bleiben Administratoren vorbehalten.',
777
+ steps: ['Benutzer wählen', 'Rolle zuordnen', 'Rechteprofil setzen', 'Löschschutz prüfen']
778
+ };
779
+ if (routeInfo().intro) return {
780
+ title: 'EOS Cockpit',
781
+ text: 'Beginne mit Systemstatus, Hosts und gesicherten Basisdiensten. Danach Module Schritt für Schritt freischalten.',
782
+ steps: ['System prüfen', 'Sicherung prüfen', 'Module planen', 'Rechte vergeben']
783
+ };
784
+ return {
785
+ title: 'EOS Bedienhilfe',
786
+ text: 'Ich unterstütze dich beim Einrichten, Prüfen und Absichern deiner EOS-Module.',
787
+ steps: ['Ziel beschreiben', 'Modul auswählen', 'Parameter prüfen', 'Test starten']
788
+ };
789
+ };
790
+
791
+ const assistAnswer = query => {
792
+ const q = normalize(query);
793
+ if (!q) return 'Beschreibe kurz, was eingerichtet werden soll, zum Beispiel: PV, Speicher, Wallbox, Modbus, Backup oder Benutzerrechte.';
794
+ if (/wallbox|evcs|lade|auto|ocpp/.test(q)) return 'Für Ladepunkte: zuerst OCPP/EVCS-Modul installieren, Verbindung zur Wallbox prüfen, Ladepunkt-ID setzen, danach Datenpunkte für Status, Leistung und Freigabe testen.';
795
+ if (/pv|solar|wechselrichter|sun2000|fronius|kostal|sma/.test(q)) return 'Für PV/Wechselrichter: IP-Adresse, Modbus/TCP oder Hersteller-API aktivieren, Abfrageintervall moderat setzen und danach Erzeugung, Bezug und Einspeisung in den Datenpunkten prüfen.';
796
+ if (/speicher|batterie|akku/.test(q)) return 'Für Speicher: Kommunikationsweg prüfen, Lade-/Entladeleistung und SoC-Datenpunkte kontrollieren und Grenzwerte erst setzen, wenn Livewerte plausibel sind.';
797
+ if (/modbus/.test(q)) return 'Für Modbus: Host/IP, Port 502, Unit-ID und Registertabelle prüfen. Danach erst lesen testen, dann Schreibrechte gezielt freischalten.';
798
+ if (/mqtt/.test(q)) return 'Für MQTT: Broker-Adresse, Zugangsdaten, Topic-Struktur und TLS prüfen. Danach mit einem Test-Topic starten und erst dann produktive Topics freigeben.';
799
+ if (/backup|sicherung|restore/.test(q)) return 'Für Sicherung: BackItUp aktiv halten, Zielpfad oder Cloud-Ziel prüfen, Test-Backup starten und Restore-Hinweise dokumentieren.';
800
+ if (/rechte|benutzer|installateur|kunde|admin/.test(q)) return 'Für Rechte: Benutzer in Rollen trennen. Installateure dürfen konfigurieren, aber geschützte EOS-Systemmodule nicht löschen. Endkunden bekommen nur Bedien- und Leserechte.';
801
+ if (/fehler|log|offline|404|timeout/.test(q)) return 'Bei Fehlern: zuerst Systemlogs öffnen, betroffene Instanz filtern, letzte Änderung prüfen, Dienst neu starten und danach Port/WebSocket/Repository prüfen.';
802
+ return 'Vorschlag: Starte mit dem passenden Modul, prüfe die Verbindung, kontrolliere die erzeugten Datenpunkte und sichere danach die Rechte. Für konkrete Hilfe nenne bitte Modulname und Zielgerät.';
803
+ };
804
+
805
+ const ensureEosAssist = () => safe(() => {
806
+ const hasApp = !!document.getElementById('app-paper');
807
+ if (!hasApp || isLoginView()) {
808
+ document.getElementById('eos-assist-root')?.remove();
809
+ return;
810
+ }
811
+ let root = document.getElementById('eos-assist-root');
812
+ if (!root) {
813
+ root = document.createElement('section');
814
+ root.id = 'eos-assist-root';
815
+ root.className = 'eos-assist-root';
816
+ document.body.appendChild(root);
817
+ }
818
+ const ctx = assistContext();
819
+ root.classList.toggle('eos-assist-open', !!state.assistOpen);
820
+ root.innerHTML = `
821
+ <button class="eos-assist-button" type="button" aria-expanded="${state.assistOpen ? 'true' : 'false'}">
822
+ <span class="eos-assist-dot"></span><strong>EOS Assist</strong><small>Einrichtungshilfe</small>
823
+ </button>
824
+ <div class="eos-assist-panel" role="dialog" aria-label="EOS Assist Einrichtungshilfe">
825
+ <div class="eos-assist-head">
826
+ <div><strong>${ctx.title}</strong><span>${ctx.text}</span></div>
827
+ <button type="button" class="eos-assist-close" aria-label="EOS Assist schließen">×</button>
828
+ </div>
829
+ <div class="eos-assist-steps">${ctx.steps.map(step => `<span>${step}</span>`).join('')}</div>
830
+ <label class="eos-assist-input-label">Was möchtest du einrichten?</label>
831
+ <div class="eos-assist-input-row"><input class="eos-assist-input" placeholder="z. B. Wallbox, PV, Modbus, Rechte..." /><button type="button" class="eos-assist-send">Fragen</button></div>
832
+ <div class="eos-assist-answer">${assistAnswer('')}</div>
833
+ </div>
834
+ `;
835
+ const openButton = root.querySelector('.eos-assist-button');
836
+ const closeButton = root.querySelector('.eos-assist-close');
837
+ const input = root.querySelector('.eos-assist-input');
838
+ const send = root.querySelector('.eos-assist-send');
839
+ const answer = root.querySelector('.eos-assist-answer');
840
+ openButton?.addEventListener('click', event => {
841
+ event.preventDefault();
842
+ state.assistOpen = !state.assistOpen;
843
+ root.classList.toggle('eos-assist-open', state.assistOpen);
844
+ openButton.setAttribute('aria-expanded', state.assistOpen ? 'true' : 'false');
845
+ if (state.assistOpen) setTimeout(() => input?.focus(), 40);
846
+ });
847
+ closeButton?.addEventListener('click', event => {
848
+ event.preventDefault();
849
+ state.assistOpen = false;
850
+ root.classList.remove('eos-assist-open');
851
+ openButton?.setAttribute('aria-expanded', 'false');
852
+ });
853
+ const ask = () => {
854
+ if (answer) answer.textContent = assistAnswer(input?.value || '');
855
+ };
856
+ send?.addEventListener('click', ask);
857
+ input?.addEventListener('keydown', event => {
858
+ if (event.key === 'Enter') ask();
859
+ });
860
+ });
861
+
731
862
  const patchDocumentMeta = () => safe(() => {
732
863
  document.title = BRAND_LONG;
733
864
  const theme = document.querySelector('meta[name="theme-color"]');
@@ -745,6 +876,8 @@
745
876
  patchDocumentMeta();
746
877
  patchLogin();
747
878
  patchShell();
879
+ applyNavCompactPreference();
880
+ ensureEosAssist();
748
881
  ensureRightsHelper();
749
882
  ensurePermissionPresets();
750
883
  ensureSettingsDialogClasses();
@@ -762,6 +895,8 @@
762
895
  normalizeBadAddressAfterLogin();
763
896
  patchLogin();
764
897
  patchShell();
898
+ applyNavCompactPreference();
899
+ ensureEosAssist();
765
900
  ensureRightsHelper();
766
901
  ensurePermissionPresets();
767
902
  ensureSettingsDialogClasses();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v26-security-visibility-guard';
4
+ const VERSION = 'v27-security-visibility-guard';
5
5
  const LEGACY_ADMIN = 'admin';
6
6
  const LEGACY_ADMIN_INSTANCE = 'admin.0';
7
7
  const SECURITY_URL = '/nexowatt/security/session';
package/build/main.js CHANGED
@@ -828,13 +828,8 @@ class Admin extends adapter_core_1.Adapter {
828
828
  }
829
829
  let changed = false;
830
830
  obj.common = obj.common || {};
831
- if (options.keepDontDelete) {
832
- if (obj.common.dontDelete !== true) {
833
- obj.common.dontDelete = true;
834
- changed = true;
835
- }
836
- }
837
- else if (obj.common.dontDelete === true && id !== `system.adapter.${EOS_ADMIN_ADAPTER_NAME}`) {
831
+ // v28: do not use common.dontDelete for EOS protection because it can block adapter upgrades.
832
+ if (obj.common.dontDelete === true) {
838
833
  delete obj.common.dontDelete;
839
834
  changed = true;
840
835
  }
@@ -870,7 +865,7 @@ class Admin extends adapter_core_1.Adapter {
870
865
  const isEosAdmin = adapter === EOS_ADMIN_ADAPTER_NAME;
871
866
  const adminOnlyAcl = isEosAdmin || this.shouldApplyAdminOnlyAclToProtectedAdapters();
872
867
  let changed = await this.ensureObjectProtectionPolicy(`system.adapter.${adapter}`, {
873
- keepDontDelete: isEosAdmin,
868
+ keepDontDelete: false,
874
869
  adminOnlyAcl,
875
870
  });
876
871
  try {
@@ -880,7 +875,7 @@ class Admin extends adapter_core_1.Adapter {
880
875
  });
881
876
  for (const row of instances.rows) {
882
877
  changed = (await this.ensureObjectProtectionPolicy(row.id, {
883
- keepDontDelete: isEosAdmin,
878
+ keepDontDelete: false,
884
879
  adminOnlyAcl,
885
880
  })) || changed;
886
881
  }
@@ -889,7 +884,7 @@ class Admin extends adapter_core_1.Adapter {
889
884
  this.log.warn(`Cannot protect instances of adapter "${adapter}": ${e instanceof Error ? e.message : e}`);
890
885
  }
891
886
  if (changed) {
892
- this.log.info(`EOS delete/ACL guard applied to adapter "${adapter}"`);
887
+ this.log.info(`EOS ACL/UI delete guard applied to adapter "${adapter}"`);
893
888
  }
894
889
  }
895
890
  async ensureLegacyAdminLocked() {