iobroker.eos-admin 7.9.35 → 7.9.36

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.
@@ -3319,3 +3319,55 @@ html.eos-app #eos-assist-root {
3319
3319
  html.eos-app .eos-assist-root,
3320
3320
  html.eos-app #eos-assist-root { right: 96px !important; }
3321
3321
  }
3322
+
3323
+
3324
+ /* === NexoWatt EOS v36: native adapter configuration safe mode =============
3325
+ Custom adapter configuration pages (React/HTML/jsonConfig) must be fully
3326
+ controlled by the adapter itself. EOS shell decoration is disabled inside the
3327
+ content area so buttons such as "Gerät hinzufügen" and "Gerät bearbeiten" stay
3328
+ clickable and dialogs/popovers are not hidden behind overlays. */
3329
+ html.eos-app.eos-adapter-config-surface #app-paper,
3330
+ html.eos-app.eos-adapter-config-surface #app-paper * {
3331
+ pointer-events: auto !important;
3332
+ }
3333
+ html.eos-app.eos-adapter-config-surface #app-paper button,
3334
+ html.eos-app.eos-adapter-config-surface #app-paper [role="button"],
3335
+ html.eos-app.eos-adapter-config-surface #app-paper a,
3336
+ html.eos-app.eos-adapter-config-surface #app-paper input,
3337
+ html.eos-app.eos-adapter-config-surface #app-paper select,
3338
+ html.eos-app.eos-adapter-config-surface #app-paper textarea,
3339
+ html.eos-app.eos-adapter-config-surface #app-paper .MuiButton-root,
3340
+ html.eos-app.eos-adapter-config-surface #app-paper .MuiIconButton-root,
3341
+ html.eos-app.eos-adapter-config-surface #app-paper .MuiMenuItem-root {
3342
+ pointer-events: auto !important;
3343
+ user-select: auto !important;
3344
+ }
3345
+ html.eos-app.eos-adapter-config-surface .eos-assist-root,
3346
+ html.eos-app.eos-adapter-config-surface #eos-assist-root,
3347
+ html.eos-app.eos-adapter-config-surface .eos-assist-config-hidden {
3348
+ display: none !important;
3349
+ visibility: hidden !important;
3350
+ pointer-events: none !important;
3351
+ }
3352
+ html.eos-app.eos-adapter-config-surface .MuiDialog-root,
3353
+ html.eos-app.eos-adapter-config-surface .MuiModal-root,
3354
+ html.eos-app.eos-adapter-config-surface .MuiPopover-root,
3355
+ html.eos-app.eos-adapter-config-surface .MuiMenu-root,
3356
+ html.eos-app.eos-adapter-config-surface .MuiDialog-container {
3357
+ pointer-events: auto !important;
3358
+ z-index: 4200 !important;
3359
+ }
3360
+ html.eos-app.eos-adapter-config-surface .MuiDialog-paper,
3361
+ html.eos-app.eos-adapter-config-surface .MuiPopover-paper,
3362
+ html.eos-app.eos-adapter-config-surface .MuiMenu-paper {
3363
+ pointer-events: auto !important;
3364
+ }
3365
+ html.eos-app.eos-adapter-config-surface #app-paper .eos-protected-delete-control,
3366
+ html.eos-app.eos-adapter-config-surface #app-paper .eos-security-hidden-delete,
3367
+ html.eos-app.eos-adapter-config-surface #app-paper .eos-protected-adapter-row {
3368
+ display: revert !important;
3369
+ visibility: visible !important;
3370
+ opacity: 1 !important;
3371
+ filter: none !important;
3372
+ pointer-events: auto !important;
3373
+ }
@@ -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=35" />
34
+ <link rel="stylesheet" href="./css/eos-branding.css?v=36" />
35
35
  <link
36
36
  rel="manifest"
37
37
  href="manifest.json"
@@ -154,10 +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=35"></script>
158
- <script defer src="./js/eos-security-ui.js?v=35"></script>
159
- <script defer src="./js/eos-hard-logout.js?v=35"></script>
160
- <script defer src="./js/eos-assistant.js?v=35"></script>
157
+ <script defer src="./js/eos-branding.js?v=36"></script>
158
+ <script defer src="./js/eos-security-ui.js?v=36"></script>
159
+ <script defer src="./js/eos-assistant.js?v=36"></script>
161
160
  </head>
162
161
  <body>
163
162
  <noscript>You need to enable JavaScript to run this app.</noscript>
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- window.NEXOWATT_EOS_UI_VERSION = 'v35-login-config-fix';
4
+ window.NEXOWATT_EOS_UI_VERSION = 'v36-native-config-session-fix';
5
5
 
6
6
  const BRAND = 'NexoWatt EOS';
7
7
  const EOS_MEANING = 'Energy Operation System';
@@ -398,6 +398,9 @@
398
398
  const applySecurityUiGuard = () => safe(() => {
399
399
  const policy = state.securityPolicy;
400
400
  applySecurityClasses();
401
+ // Do not apply EOS security decoration inside native adapter configuration pages.
402
+ // Adapter UIs must remain 100% functional; backend/role checks still protect EOS actions.
403
+ if (isAdapterConfigSurface()) return;
401
404
  if (!policy.loaded) return;
402
405
  if (isAdminUser()) {
403
406
  document.querySelectorAll('.eos-hidden-legacy-admin, .eos-protected-adapter-row').forEach(el => {
@@ -470,16 +473,33 @@
470
473
  };
471
474
 
472
475
 
476
+ const isAdapterConfigSurface = () => document.documentElement.classList.contains('eos-adapter-config-surface');
477
+
473
478
  const markAdapterConfigSurface = () => safe(() => {
474
- const text = textOfElement(document.querySelector('#app-paper') || document.body).slice(0, 1500);
475
- const isConfig = /Instanzeinstellungen:|Instance settings:|Gerät hinzufügen|Gerät bearbeiten|Adapterkonfiguration|json exportieren/i.test(text)
476
- || /tab-instances/i.test(window.location.hash || '');
479
+ const app = document.querySelector('#app-paper') || document.body;
480
+ const text = textOfElement(app).slice(0, 3500);
481
+ const isConfig = /Instanzeinstellungen:|Instance settings:|Gerät hinzufügen|Gerät bearbeiten|Geräteliste|Adapterkonfiguration|json exportieren|json importieren|Speichern und schließen|Save and close/i.test(text);
477
482
  document.documentElement.classList.toggle('eos-adapter-config-surface', !!isConfig);
478
483
  if (!isConfig) return;
479
- // Do not let EOS decorative layers influence adapter specific buttons/dialogs.
480
- document.querySelectorAll('#app-paper button, #app-paper [role="button"], #app-paper a, #app-paper input, #app-paper select, #app-paper textarea').forEach(control => {
484
+
485
+ // Native adapter configuration UIs are owned by the adapter. EOS must never
486
+ // block, rewrite or intercept their controls. This is critical for custom
487
+ // React/HTML configuration pages such as nexowatt-devices.
488
+ document.querySelectorAll('#app-paper button, #app-paper [role="button"], #app-paper a, #app-paper input, #app-paper select, #app-paper textarea, #app-paper [tabindex]').forEach(control => {
481
489
  if (control.closest('.eos-assist-root, #eos-assist-root, .eos-standalone-nav-toggle')) return;
482
490
  control.style.pointerEvents = 'auto';
491
+ control.removeAttribute('aria-disabled');
492
+ });
493
+ document.querySelectorAll('#app-paper .eos-protected-delete-control, #app-paper .eos-security-hidden-delete, #app-paper .eos-protected-adapter-row').forEach(el => {
494
+ el.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete', 'eos-protected-adapter-row');
495
+ el.removeAttribute('aria-disabled');
496
+ if (el.style) {
497
+ el.style.pointerEvents = '';
498
+ el.style.display = '';
499
+ el.style.visibility = '';
500
+ el.style.opacity = '';
501
+ }
502
+ if ('disabled' in el && !el.dataset.eosOriginalDisabled) el.disabled = false;
483
503
  });
484
504
  });
485
505
 
@@ -592,13 +612,10 @@
592
612
  }
593
613
 
594
614
  const installHardLogoutWatchdog = () => {
595
- if (hardLogoutInstalled) return;
615
+ // v36: disabled. Upstream ioBroker Admin session handling is used again so
616
+ // the configured admin TTL is respected and native adapter config pages are
617
+ // not interrupted by duplicate EOS timers.
596
618
  hardLogoutInstalled = true;
597
- window.addEventListener('focus', () => checkHardLogoutSession(), { passive: true });
598
- document.addEventListener('visibilitychange', () => {
599
- if (!document.hidden) checkHardLogoutSession();
600
- }, { passive: true });
601
- scheduleHardLogoutCheck(2500);
602
619
  };
603
620
 
604
621
  const ensureBrandBadge = toolbar => {
@@ -1009,6 +1026,11 @@
1009
1026
  document.querySelectorAll('.eos-assist-launcher,.eos-assist-panel:not(#eos-assist-root .eos-assist-panel)').forEach(el => el.remove());
1010
1027
  return;
1011
1028
  }
1029
+ if (isAdapterConfigSurface()) {
1030
+ const existing = document.getElementById('eos-assist-root');
1031
+ if (existing) existing.classList.add('eos-assist-config-hidden');
1032
+ return;
1033
+ }
1012
1034
 
1013
1035
  let root = document.getElementById('eos-assist-root');
1014
1036
  if (!root) {
@@ -1042,6 +1064,7 @@
1042
1064
  document.body.appendChild(root);
1043
1065
  }
1044
1066
 
1067
+ root.classList.remove('eos-assist-config-hidden');
1045
1068
  const ctx = assistContext();
1046
1069
  const button = root.querySelector('.eos-assist-button');
1047
1070
  const input = root.querySelector('.eos-assist-input');
@@ -1203,8 +1226,17 @@
1203
1226
  hideNativeLogoutNav();
1204
1227
  hideOfficialNexoWattRepoWarning();
1205
1228
  applySecurityUiGuard();
1206
- patchTextNodes(document.body || document.documentElement);
1207
- patchAttributes(document.body || document.documentElement);
1229
+ if (isAdapterConfigSurface()) {
1230
+ ['.MuiAppBar-root', '.MuiDrawer-paper', 'nav', '.eos-brand-badge', '.eos-top-toolbar'].forEach(selector => {
1231
+ document.querySelectorAll(selector).forEach(scope => {
1232
+ patchTextNodes(scope);
1233
+ patchAttributes(scope);
1234
+ });
1235
+ });
1236
+ } else {
1237
+ patchTextNodes(document.body || document.documentElement);
1238
+ patchAttributes(document.body || document.documentElement);
1239
+ }
1208
1240
  };
1209
1241
 
1210
1242
  const scopePatch = () => {
@@ -1228,6 +1260,7 @@
1228
1260
  applySecurityUiGuard();
1229
1261
  for (const scope of scopes.slice(0, 80)) {
1230
1262
  if (!scope || !scope.isConnected) continue;
1263
+ if (isAdapterConfigSurface() && (scope.id === 'app-paper' || scope.closest?.('#app-paper'))) continue;
1231
1264
  patchTextNodes(scope);
1232
1265
  patchAttributes(scope);
1233
1266
  }
@@ -1,165 +1,18 @@
1
1
  (function () {
2
2
  'use strict';
3
-
4
- const LOG_PREFIX = '[NexoWatt EOS hard logout]';
5
- const VERSION = '35';
6
- const MIN_POLL_MS = 15_000;
7
- const MAX_TIMER_MS = 2_147_000_000;
8
- const MIN_TTL_SEC = 5;
9
- const STORAGE_KEY = `nexowatt:eos:hardLogoutAt:${location.host}:root`;
10
- let logoutTimer = null;
11
- let pollTimer = null;
12
- let logoutStarted = false;
13
-
14
- function isLoginPage() {
15
- const path = String(window.location.pathname || '').toLowerCase();
16
- const search = String(window.location.search || '').toLowerCase();
17
- return path.endsWith('/login') || path.includes('/login/') || search.includes('login');
18
- }
19
-
20
- function clearStoredTokens() {
21
- const storages = [window.localStorage, window.sessionStorage, window._localStorage, window._sessionStorage]
22
- .filter(Boolean)
23
- .filter((storage, index, array) => array.indexOf(storage) === index);
24
- const tokenKeyPattern = /(access[_-]?token|refresh[_-]?token|token[_-]?expires|expires[_-]?in|oauth|bearer|auth|connection)/i;
25
- for (const storage of storages) {
26
- try {
27
- const keys = [];
28
- for (let i = 0; i < storage.length; i++) {
29
- const key = storage.key(i);
30
- if (key && (tokenKeyPattern.test(key) || key === STORAGE_KEY)) {
31
- keys.push(key);
32
- }
33
- }
34
- keys.forEach(key => storage.removeItem(key));
35
- } catch (e) {
36
- // ignore blocked storage
37
- }
38
- }
39
- }
40
-
41
- function clearAuthCookies() {
42
- const cookieNames = ['access_token', 'refresh_token', 'connect.sid', 'ioBroker.sid'];
43
- const paths = ['/', window.location.pathname.replace(/\/[^/]*$/, '/') || '/'];
44
- for (const name of cookieNames) {
45
- for (const path of paths) {
46
- document.cookie = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}; SameSite=Lax`;
47
- }
48
- }
49
- }
50
-
51
- function readDeadline() {
52
- try {
53
- const value = Number(window.localStorage.getItem(STORAGE_KEY));
54
- return Number.isFinite(value) && value > 0 ? value : 0;
55
- } catch (e) {
56
- return 0;
57
- }
58
- }
59
-
60
- function writeDeadline(deadline) {
61
- try {
62
- window.localStorage.setItem(STORAGE_KEY, String(deadline));
63
- } catch (e) {
64
- // ignore blocked storage
65
- }
66
- }
67
-
68
- function hardLogout(reason) {
69
- if (logoutStarted || isLoginPage()) return;
70
- logoutStarted = true;
71
- try { console.warn(LOG_PREFIX, reason || 'session expired'); } catch (e) {}
72
- if (logoutTimer) {
73
- clearTimeout(logoutTimer);
74
- logoutTimer = null;
75
- }
76
- if (pollTimer) {
77
- clearInterval(pollTimer);
78
- pollTimer = null;
79
- }
80
- clearStoredTokens();
81
- clearAuthCookies();
82
- const logoutUrl = new URL('logout', window.location.origin + '/');
83
- logoutUrl.searchParams.set('hard', '1');
84
- logoutUrl.searchParams.set('ts', String(Date.now()));
85
- window.location.replace(logoutUrl.href);
86
- }
87
-
88
- function scheduleHardLogout(deadline) {
89
- const ms = Math.min(Math.max(deadline - Date.now() + 250, 250), MAX_TIMER_MS);
90
- if (logoutTimer) clearTimeout(logoutTimer);
91
- logoutTimer = setTimeout(() => hardLogout('configured login timeout reached'), ms);
92
- }
93
-
94
- function applyServerExpiration(expireInSec) {
95
- const seconds = Number(expireInSec);
96
- if (!Number.isFinite(seconds)) return;
97
- if (seconds <= 0) {
98
- hardLogout('server reported expired session');
99
- return;
100
- }
101
- if (seconds < MIN_TTL_SEC) return;
102
-
103
- const now = Date.now();
104
- const candidateDeadline = now + seconds * 1000;
105
- const storedDeadline = readDeadline();
106
-
107
- // Set the hard deadline once. If the server reports an earlier expiration later,
108
- // tighten the deadline. Never extend it through refresh-token based renewal.
109
- const deadline = !storedDeadline || candidateDeadline < storedDeadline - 5000 ? candidateDeadline : storedDeadline;
110
- if (deadline !== storedDeadline) writeDeadline(deadline);
111
-
112
- if (now >= deadline) {
113
- hardLogout('stored hard deadline reached');
114
- } else {
115
- scheduleHardLogout(deadline);
116
- }
117
- }
118
-
119
- async function checkSession() {
120
- if (logoutStarted) return;
121
- if (isLoginPage()) {
122
- clearStoredTokens();
123
- clearAuthCookies();
124
- return;
125
- }
126
-
127
- const storedDeadline = readDeadline();
128
- if (storedDeadline && Date.now() >= storedDeadline) {
129
- hardLogout('stored hard deadline reached');
130
- return;
131
- }
132
-
133
- try {
134
- const response = await fetch('./session?hard=1&ts=' + Date.now(), {
135
- credentials: 'include',
136
- cache: 'no-store',
137
- headers: { Accept: 'application/json' },
138
- });
139
- if (!response.ok) return;
140
- const session = await response.json();
141
- if (typeof session.expireInSec === 'number') {
142
- applyServerExpiration(session.expireInSec);
143
- }
144
- } catch (e) {
145
- // During update/restart the endpoint can be unavailable. Do not logout just because of a network error.
146
- }
147
- }
148
-
149
- function start() {
150
- checkSession();
151
- pollTimer = setInterval(checkSession, MIN_POLL_MS);
152
- window.addEventListener('focus', checkSession, { passive: true });
153
- document.addEventListener('visibilitychange', () => {
154
- if (!document.hidden) checkSession();
155
- });
156
- }
157
-
158
- window.NEXOWATT_EOS_HARD_LOGOUT = { version: VERSION, checkSession, hardLogout, clearStoredTokens, clearAuthCookies };
159
-
160
- if (document.readyState === 'loading') {
161
- document.addEventListener('DOMContentLoaded', start, { once: true });
162
- } else {
163
- start();
164
- }
3
+ // NexoWatt EOS v36: compatibility stub.
4
+ // The custom hard-logout timer was removed because it could expire sessions
5
+ // earlier than the configured admin TTL and broke native adapter dialogs.
6
+ // Session handling is now delegated to the same OAuth/session flow as the
7
+ // upstream ioBroker Admin.
8
+ const VERSION = '36';
9
+ function noop() {}
10
+ window.NEXOWATT_EOS_HARD_LOGOUT = {
11
+ version: VERSION,
12
+ checkSession: noop,
13
+ hardLogout: noop,
14
+ clearStoredTokens: noop,
15
+ clearAuthCookies: noop,
16
+ disabled: true,
17
+ };
165
18
  })();
@@ -220,6 +220,7 @@
220
220
  };
221
221
 
222
222
  const applyPolicyToDom = () => {
223
+ if (isAdapterConfigSurface()) return;
223
224
  const admin = isAdminUser();
224
225
  document.documentElement.classList.toggle('eos-security-admin-user', admin);
225
226
  document.documentElement.classList.toggle('eos-security-non-admin-user', !admin);
@@ -230,6 +231,8 @@
230
231
  hideEosSecuritySettingsForNonAdmins();
231
232
  };
232
233
 
234
+ const isAdapterConfigSurface = () => document.documentElement.classList.contains('eos-adapter-config-surface') || /Instanzeinstellungen:|Instance settings:|Geräteliste|Gerät hinzufügen|Gerät bearbeiten/i.test(document.body?.textContent || '');
235
+
233
236
  const scheduleApply = () => {
234
237
  if (state.scheduled) return;
235
238
  state.scheduled = true;
@@ -267,7 +270,7 @@
267
270
 
268
271
  document.addEventListener('click', event => {
269
272
  const target = event.target?.closest?.('button,[role="button"],a,[role="menuitem"],.MuiMenuItem-root');
270
- if (!target || isAdminUser()) return;
273
+ if (!target || isAdminUser() || isAdapterConfigSurface()) return;
271
274
  const label = normalizeFlat(`${target.textContent || ''} ${target.getAttribute?.('title') || ''} ${target.getAttribute?.('aria-label') || ''}`);
272
275
  if (/loschen|delete|remove|deinstall|uninstall/.test(label)) {
273
276
  target.classList.add('eos-security-hidden-delete');
package/build/lib/web.js CHANGED
@@ -583,6 +583,16 @@ class Web {
583
583
  this.server.app.get('/version', (_req, res) => {
584
584
  res.status(200).send(this.adapter.version);
585
585
  });
586
+ // v36: Repair stale/broken URLs generated by older EOS hard-logout builds.
587
+ this.server.app.use((req, res, next) => {
588
+ const requested = String(req.originalUrl || req.url || '');
589
+ if (/(?:%2f|%252f)(?:%23|%2523)|\/login\/|\/logout\/|hard=1/i.test(requested)
590
+ && /index\.html|login|hard=1|origin=|%23|%2523/i.test(requested)) {
591
+ res.redirect(this.LOGIN_PAGE);
592
+ return;
593
+ }
594
+ next();
595
+ });
586
596
  // replace socket.io
587
597
  this.server.app.use((req, res, next) => {
588
598
  const url = req.url.split('?')[0];
@@ -633,38 +643,12 @@ class Web {
633
643
  this.server.app.use(cookieParser());
634
644
  this.server.app.use(bodyParser.urlencoded({ extended: true }));
635
645
  this.server.app.use(bodyParser.json());
636
- // NexoWatt EOS: enforce the configured login timeout as a hard logout.
637
- // We remove refresh tokens from OAuth responses so the admin client cannot silently extend sessions.
638
- const eosHardLogout = false; // v35: keep OAuth refresh flow alive; hard timeout is enforced client-side by an absolute deadline.
639
- if (eosHardLogout) {
640
- this.server.app.use('/oauth/token', (req, res, next) => {
641
- const grantType = String(req.body?.grant_type || '');
642
- if (req.method === 'POST' && grantType === 'refresh_token') {
643
- res.status(401).json({
644
- error: 'invalid_grant',
645
- error_description: 'EOS session expired. Please log in again.',
646
- eosHardLogout: true,
647
- });
648
- return;
649
- }
650
- const originalJson = res.json.bind(res);
651
- res.json = body => {
652
- if (body && typeof body === 'object' && body.access_token && body.expires_in) {
653
- const expiresIn = Math.max(1, Math.round(Number(body.expires_in) || Number(this.settings.ttl) || 3600));
654
- body.refresh_token = '';
655
- body.refresh_token_expires_in = expiresIn;
656
- body.eosHardLogout = true;
657
- }
658
- return originalJson(body);
659
- };
660
- next();
661
- });
662
- }
646
+ // v36: OAuth/session handling intentionally follows upstream ioBroker Admin.
663
647
  this.oauth2Model = (0, webserver_1.createOAuth2Server)(this.adapter, {
664
648
  app: this.server.app,
665
649
  secure: this.settings.secure,
666
650
  accessLifetime: this.settings.ttl,
667
- refreshLifetime: this.settings.ttl,
651
+ refreshLifetime: 60 * 60 * 24 * 7, // 1 week, same as upstream admin
668
652
  noBasicAuth: this.settings.noBasicAuth,
669
653
  loginPage: (req) => {
670
654
  const isDev = req.url.includes('?dev');
@@ -676,44 +660,44 @@ class Web {
676
660
  },
677
661
  });
678
662
  this.server.app.get('/session', (req, res) => {
679
- res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
680
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
681
- if (!this.settings.auth) {
682
- res.json({ expireInSec: Math.max(120, this.settings.ttl || 3600), hardLogout: true, auth: false });
683
- return;
684
- }
685
- const tokenValue = this.readAccessTokenFromRequest(req);
686
- if (!tokenValue) {
687
- res.json({ expireInSec: 0, hardLogout: true, error: 'Cannot find session token' });
688
- return;
689
- }
690
- const candidates = new Set();
691
- candidates.add(tokenValue);
692
- candidates.add(tokenValue.startsWith('a:') ? tokenValue : `a:${tokenValue}`);
693
- if (tokenValue.length > 1) candidates.add(`a:${tokenValue[1]}`);
694
- const ids = Array.from(candidates);
695
- const readNext = (index) => {
696
- const id = ids[index];
697
- if (!id) {
698
- res.json({ expireInSec: 0, hardLogout: true, error: 'Cannot find session' });
663
+ // v36: Follow upstream admin semantics again. Do not run a second
664
+ // EOS hard-logout timer here; the official OAuth/session handling
665
+ // already uses the configured access lifetime.
666
+ if (req.headers.cookie) {
667
+ const cookies = req.headers.cookie.split(';').find(c => c.trim().startsWith('access_token='));
668
+ let tokenCookie = cookies?.split('=')[1];
669
+ if (!tokenCookie && req.headers.authorization?.startsWith('Bearer ')) {
670
+ tokenCookie = req.headers.authorization.split(' ')[1];
671
+ }
672
+ else if (!tokenCookie && req.query?.token) {
673
+ tokenCookie = req.query.token;
674
+ }
675
+ if (tokenCookie) {
676
+ const candidates = new Set();
677
+ candidates.add(tokenCookie.startsWith('a:') ? tokenCookie : `a:${tokenCookie}`);
678
+ if (tokenCookie.length > 1)
679
+ candidates.add(`a:${tokenCookie[1]}`);
680
+ const ids = Array.from(candidates);
681
+ const readNext = (index) => {
682
+ const id = ids[index];
683
+ if (!id) {
684
+ res.json({ expireInSec: 0 });
685
+ return;
686
+ }
687
+ void this.adapter.getSession(id, (token) => {
688
+ if (!token?.user) {
689
+ readNext(index + 1);
690
+ }
691
+ else {
692
+ res.json({ expireInSec: Math.round((token.aExp - Date.now()) / 1000) });
693
+ }
694
+ });
695
+ };
696
+ readNext(0);
699
697
  return;
700
698
  }
701
- void this.adapter.getSession(id, (token) => {
702
- if (!token?.user) {
703
- readNext(index + 1);
704
- return;
705
- }
706
- const now = Date.now();
707
- const expirations = [Number(token.aExp), Number(token.rExp), Number(token.expire), Number(token.expires), Number(token.expiresAt)].filter(value => Number.isFinite(value) && value > 0);
708
- const expiresAt = expirations.length ? Math.min(...expirations) : now + (this.settings.ttl || 3600) * 1000;
709
- res.json({
710
- expireInSec: Math.max(0, Math.floor((expiresAt - now) / 1000)),
711
- hardLogout: true,
712
- user: token.user,
713
- });
714
- });
715
- };
716
- readNext(0);
699
+ }
700
+ res.json({ error: 'Cannot find session' });
717
701
  });
718
702
  this.server.app.get(/.*\/nexowatt\/security\/(?:context|session)$/, (req, res) => {
719
703
  void this.sendEosSecuritySession(req, res).catch(e => {
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "eos-admin",
4
- "version": "7.9.35",
4
+ "version": "7.9.36",
5
5
  "titleLang": {
6
6
  "en": "NexoWatt EOS Admin",
7
7
  "de": "NexoWatt EOS Admin",
@@ -18,9 +18,13 @@
18
18
  "connectionType": "local",
19
19
  "dataSource": "push",
20
20
  "news": {
21
+ "7.9.36": {
22
+ "en": "Restores official admin session handling, fixes bad login redirect paths, and enables full native adapter configuration compatibility without EOS overlays blocking adapter UI actions.",
23
+ "de": "Stellt die offizielle Admin-Sitzungslogik wieder her, korrigiert fehlerhafte Login-Redirect-Pfade und aktiviert volle native Adapter-Konfigurationskompatibilität ohne blockierende EOS-Overlays."
24
+ },
21
25
  "7.9.35": {
22
- "en": "Fixed repository metadata for reliable EOS Admin updates and enforced hard logout after the configured login timeout.",
23
- "de": "Repository-Metadaten für zuverlässige EOS-Admin-Updates korrigiert und harte Abmeldung nach der eingestellten Abmeldezeit erzwungen."
26
+ "en": "Fixes EOS Admin login redirect/logout origin handling and restores reliable interaction with adapter instance configuration dialogs.",
27
+ "de": "Korrigiert den EOS-Admin Login-/Logout-Redirect und stellt die Bedienung von Adapter-Instanzkonfigurationen wieder zuverlässig her."
24
28
  },
25
29
  "7.9.31": {
26
30
  "en": "UI cleanup: fixes EOS Security German text encoding, removes the logout tile next to the compact navigation arrow, and expands EOS Assist with AI-ready multi-path setup guidance.",
@@ -165,10 +169,6 @@
165
169
  "pl": "Rozszerzony plik konfiguracyjny JSON do adaptera fregata",
166
170
  "uk": "Розширений налаштування JSON для адаптера фригату",
167
171
  "zh-cn": "JSON 护卫舰适配器扩展配置"
168
- },
169
- "7.9.35": {
170
- "en": "Fixes EOS Admin login redirect/logout origin handling and restores reliable interaction with adapter instance configuration dialogs.",
171
- "de": "Korrigiert den EOS-Admin Login-/Logout-Redirect und stellt die Bedienung von Adapter-Instanzkonfigurationen wieder zuverlässig her."
172
172
  }
173
173
  },
174
174
  "desc": {
@@ -206,7 +206,7 @@
206
206
  "icon": "admin.svg",
207
207
  "messagebox": true,
208
208
  "enabled": true,
209
- "extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.35/admin/admin.svg",
209
+ "extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.36/admin/admin.svg",
210
210
  "keywords": [
211
211
  "NexoWatt",
212
212
  "EOS",
@@ -217,7 +217,7 @@
217
217
  "licensed"
218
218
  ],
219
219
  "compact": true,
220
- "readme": "https://unpkg.com/iobroker.eos-admin@7.9.35/README.md",
220
+ "readme": "https://unpkg.com/iobroker.eos-admin@7.9.36/README.md",
221
221
  "authors": [
222
222
  "bluefox <bluefox@ccu.io>",
223
223
  "hobbyquaker <hq@ccu.io>"
@@ -286,7 +286,7 @@
286
286
  "nondeletable": false,
287
287
  "allowAdapterUpdate": true,
288
288
  "allowAdapterDelete": false,
289
- "meta": "https://unpkg.com/iobroker.eos-admin@7.9.35/io-package.json",
289
+ "meta": "https://unpkg.com/iobroker.eos-admin@7.9.36/io-package.json",
290
290
  "npmPackage": "iobroker.eos-admin"
291
291
  },
292
292
  "native": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iobroker.eos-admin",
3
3
  "description": "NexoWatt EOS Admin standalone interface for Energy Operation System",
4
- "version": "7.9.35",
4
+ "version": "7.9.36",
5
5
  "contributors": [
6
6
  "bluefox <dogafox@gmail.com>",
7
7
  "apollon77",
package/public/404.html CHANGED
@@ -64,6 +64,18 @@
64
64
  color: #fff;
65
65
  }
66
66
  </style>
67
+
68
+ <script id="NEXOWATT_EOS_BAD_PATH_FIX">
69
+ (function () {
70
+ try {
71
+ var u = String(location.pathname + location.search + location.hash);
72
+ if (/(%2F%23|%252F%2523|hard=1|\/login\/|\/logout\/)/i.test(u)) {
73
+ location.replace('/index.html?login');
74
+ }
75
+ } catch (e) {}
76
+ })();
77
+ </script>
78
+
67
79
  </head>
68
80
 
69
81
  <body>
@@ -56,8 +56,11 @@ if (!bootstrap.includes('window.adapterName="eos-admin"')) fail('frontend bootst
56
56
  if (bootstrap.includes('window.adapterName="admin"')) fail('frontend bootstrap still contains window.adapterName="admin"');
57
57
 
58
58
  const webBuild = fs.readFileSync(path.join(root, 'build/lib/web.js'), 'utf8');
59
- if (!webBuild.includes("body.refresh_token = '';")) fail('build/lib/web.js does not remove OAuth refresh tokens for hard logout');
60
- if (!webBuild.includes("grantType === 'refresh_token'")) fail('build/lib/web.js does not reject refresh_token grants');
61
- if (!webBuild.includes('refreshLifetime: this.settings.ttl')) fail('build/lib/web.js does not bind refresh lifetime to ttl');
59
+ const branding = fs.readFileSync(path.join(root, 'adminWww/js/eos-branding.js'), 'utf8');
60
+ if (!webBuild.includes('refreshLifetime: 60 * 60 * 24 * 7')) fail('build/lib/web.js must keep upstream-compatible refresh lifetime');
61
+ if (!webBuild.includes('Follow upstream admin semantics again')) fail('build/lib/web.js does not contain the v36 session compatibility fix');
62
+ if (index.includes('eos-hard-logout.js')) fail('adminWww/index.html must not load the removed custom hard-logout timer');
63
+ if (!branding.includes('isAdapterConfigSurface')) fail('eos-branding.js lacks native adapter config safe-mode detection');
64
+ if (!branding.includes('Adapter UIs must remain 100% functional')) fail('eos-branding.js lacks native adapter config interaction guard');
62
65
 
63
66
  console.log('[NexoWatt EOS package validation] OK');