iobroker.eos-admin 7.9.89 → 7.9.92
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.
- package/INSTALL_TEST_V7.9.92.md +18 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +19 -25
- package/PUBLISH_STABLE_V7.9.92.md +19 -0
- package/README_STABILITY_V7.9.92.md +14 -0
- package/RELEASE_ACCEPTANCE_V7.9.92.md +12 -0
- package/RELEASE_NOTES_V7.9.92.md +17 -0
- package/adminWww/assets/Instances-YdaGnS5a-v84.js +1 -1
- package/adminWww/css/nexowatt-native-shell.css +151 -12
- package/adminWww/index.html +11 -11
- package/adminWww/js/eos-account-management.js +1 -1
- package/adminWww/js/eos-assistant.js +1 -1
- package/adminWww/js/eos-basic-settings.js +1 -1
- package/adminWww/js/eos-branding-sanitizer.js +1 -1
- package/adminWww/js/eos-native-security.js +1 -1
- package/adminWww/js/eos-policy-client.js +1 -1
- package/adminWww/js/eos-role-bootstrap.js +180 -46
- package/adminWww/js/eos-role-ui.js +156 -23
- package/adminWww/js/nexowatt-native-shell.js +1 -1
- package/build/lib/web.js +39 -0
- package/io-package.json +15 -5
- package/package.json +21 -13
- package/tools/nexowatt-account-management-selftest.cjs +1 -1
- package/tools/nexowatt-ensure-release-defaults.cjs +72 -0
- package/tools/nexowatt-internal-reserve-selftest.cjs +1 -0
- package/tools/nexowatt-login-layout-selftest.cjs +81 -0
- package/tools/nexowatt-modern-ui-selftest.cjs +7 -5
- package/tools/nexowatt-release-defaults-selftest.cjs +29 -0
- package/tools/nexowatt-validate-package.cjs +18 -8
- package/INSTALL_TEST_V7.9.89.md +0 -73
- package/PUBLISH_STABLE_V7.9.89.md +0 -33
- package/README_STABILITY_V7.9.89.md +0 -26
- package/RELEASE_ACCEPTANCE_V7.9.89.md +0 -40
- package/admin/img/eos/nexowatt-192-maskable.png +0 -0
- package/admin/img/eos/nexowatt-24.png +0 -0
- package/admin/img/eos/nexowatt-512-maskable.png +0 -0
- package/admin/img/eos/nexowatt-eos-logo.png +0 -0
- package/adminWww/admin.png +0 -0
- package/adminWww/img/eos/nexowatt-24.png +0 -0
- package/adminWww/img/eos/nexowatt-eos-logo.png +0 -0
- package/adminWww/img/eos/nexowatt-header-128.png +0 -0
- package/adminWww/img/eos/nexowatt-header-192.png +0 -0
- package/adminWww/img/eos/nexowatt-header-256.png +0 -0
- package/adminWww/img/eos/nexowatt-header-512.png +0 -0
- package/adminWww/img/eos/nexowatt-header-64.png +0 -0
- package/adminWww/img/logo192.png +0 -0
- package/adminWww/js/eos-adapter-surface-cleanup.js +0 -141
- package/adminWww/js/eos-toolbar-ghost-cleanup.js +0 -102
- package/docs/NEXOWATT_EOS_BRANDING_DE.md +0 -29
- package/docs/NEXOWATT_EOS_UI_LOGO_LOGIN_V6_DE.md +0 -9
- package/docs/NEXOWATT_EOS_UI_PERFORMANCE_V5_DE.md +0 -25
- package/docs/NEXOWATT_EOS_UI_V12_HEADER_LOGO_NAV_CLEANUP_DE.md +0 -16
- package/docs/NEXOWATT_EOS_UI_V12_HEADER_NAV_LOGO_FIX_DE.md +0 -16
- package/docs/NEXOWATT_EOS_UI_V15_DE.md +0 -8
- package/docs/NEXOWATT_EOS_UI_V16_DE.md +0 -9
- package/docs/NEXOWATT_EOS_UI_V17_DE.md +0 -24
- package/docs/NEXOWATT_EOS_UI_V6_FIXES_DE.md +0 -13
- package/docs/NEXOWATT_EOS_UI_V6_LOGIN_LOGO_404_FIX_DE.md +0 -9
- package/docs/NEXOWATT_EOS_UI_V6_LOGO_LOGIN_FIX_DE.md +0 -10
- package/docs/NEXOWATT_EOS_UI_V7_RESTORE_LOGO_SCOPE_FIX_DE.md +0 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const VERSION = '
|
|
4
|
+
const VERSION = 'v91-final-first-login-submit-guard';
|
|
5
5
|
const script = document.currentScript || document.querySelector('script[src*="eos-role-bootstrap.js"]');
|
|
6
6
|
const entry = script?.dataset?.eosEntry || '';
|
|
7
7
|
let launched = false;
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
claimLauncher: 'Erstanmeldung', claimTitle: 'Konto erstmals aktivieren',
|
|
86
86
|
claimIntro: 'Wähle Installateur oder Gast/Endkunde und melde dich bei der allerersten Anmeldung einmalig mit leerem Passwort an. Danach muss sofort ein persönliches Passwort vergeben werden.',
|
|
87
87
|
claimAccount: 'Kontoname', claimContinue: 'Erstanmeldung starten', claimChecking: 'Konto wird geprüft …',
|
|
88
|
-
claimHint: 'Bei der ersten Anmeldung das Passwortfeld leer lassen.', claimUnavailable: 'Die Erstanmeldung ist für dieses Konto nicht verfügbar oder wurde bereits abgeschlossen.',
|
|
88
|
+
claimHint: 'Bei der ersten Anmeldung das Passwortfeld leer lassen.', claimAlreadyActive: 'Für dieses Konto ist bereits ein persönliches Passwort gesetzt. Bitte Passwort eingeben.', claimStatusError: 'Der Status der Erstanmeldung konnte nicht geprüft werden.', claimUnavailable: 'Die Erstanmeldung ist für dieses Konto nicht verfügbar oder wurde bereits abgeschlossen.',
|
|
89
89
|
claimPrivate: 'Die Erstanmeldung ist nur im lokalen Netzwerk möglich.', claimExpired: 'Die Aktivierung ist abgelaufen. Bitte erneut starten.',
|
|
90
90
|
claimReady: 'Konto bestätigt. Lege jetzt dein persönliches Passwort fest.', claimSuccess: 'Passwort gespeichert. Du kannst dich jetzt normal anmelden.',
|
|
91
91
|
claimLogin: 'Zur normalen Anmeldung', claimBack: 'Zurück',
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
claimLauncher: 'First sign-in', claimTitle: 'Activate account for the first time',
|
|
105
105
|
claimIntro: 'Choose Installer or Guest/End User and leave the password field empty for the very first sign-in. A personal password must be created immediately afterwards.',
|
|
106
106
|
claimAccount: 'Account name', claimContinue: 'Start first sign-in', claimChecking: 'Checking account …',
|
|
107
|
-
claimHint: 'Leave the password field empty only for the first sign-in.', claimUnavailable: 'First activation is unavailable for this account or has already been completed.',
|
|
107
|
+
claimHint: 'Leave the password field empty only for the first sign-in.', claimAlreadyActive: 'This account already has a personal password. Enter the password to sign in.', claimStatusError: 'The first-activation status could not be checked.', claimUnavailable: 'First activation is unavailable for this account or has already been completed.',
|
|
108
108
|
claimPrivate: 'First activation is only available on the local network.', claimExpired: 'The activation has expired. Please start again.',
|
|
109
109
|
claimReady: 'Account confirmed. Create your personal password now.', claimSuccess: 'Password saved. You can now sign in normally.',
|
|
110
110
|
claimLogin: 'Return to normal sign-in', claimBack: 'Back',
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
claimLauncher: 'Eerste aanmelding', claimTitle: 'Account voor het eerst activeren',
|
|
124
124
|
claimIntro: 'Kies Installateur of Gast/Eindgebruiker en laat bij de allereerste aanmelding het wachtwoordveld leeg. Daarna moet direct een persoonlijk wachtwoord worden ingesteld.',
|
|
125
125
|
claimAccount: 'Accountnaam', claimContinue: 'Eerste aanmelding starten', claimChecking: 'Account wordt gecontroleerd …',
|
|
126
|
-
claimHint: 'Laat het wachtwoordveld alleen bij de eerste aanmelding leeg.', claimUnavailable: 'Eerste activering is voor dit account niet beschikbaar of al voltooid.',
|
|
126
|
+
claimHint: 'Laat het wachtwoordveld alleen bij de eerste aanmelding leeg.', claimAlreadyActive: 'Dit account heeft al een persoonlijk wachtwoord. Voer het wachtwoord in.', claimStatusError: 'De status van de eerste activering kon niet worden gecontroleerd.', claimUnavailable: 'Eerste activering is voor dit account niet beschikbaar of al voltooid.',
|
|
127
127
|
claimPrivate: 'Eerste activering is alleen mogelijk in het lokale netwerk.', claimExpired: 'De activering is verlopen. Start opnieuw.',
|
|
128
128
|
claimReady: 'Account bevestigd. Stel nu je persoonlijke wachtwoord in.', claimSuccess: 'Wachtwoord opgeslagen. Je kunt nu normaal aanmelden.',
|
|
129
129
|
claimLogin: 'Naar normale aanmelding', claimBack: 'Terug',
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
<p class="eos-first-login-intro"></p>
|
|
151
151
|
<div class="eos-first-login-account"><span></span><strong></strong></div>
|
|
152
152
|
<form novalidate>
|
|
153
|
+
<input name="username" type="text" autocomplete="username" hidden />
|
|
153
154
|
<label><span class="password-label"></span><input name="password" type="password" autocomplete="new-password" maxlength="128" required /></label>
|
|
154
155
|
<label><span class="repeat-label"></span><input name="passwordRepeat" type="password" autocomplete="new-password" maxlength="128" required /></label>
|
|
155
156
|
<label class="eos-first-login-show"><input name="show" type="checkbox" /><span></span></label>
|
|
@@ -171,6 +172,7 @@
|
|
|
171
172
|
overlay.querySelector('.eos-first-login-submit').textContent = t.submit;
|
|
172
173
|
overlay.querySelector('.eos-first-login-logout').textContent = t.logout;
|
|
173
174
|
const form = overlay.querySelector('form');
|
|
175
|
+
form.elements.username.value = claim.userName || 'guest';
|
|
174
176
|
const password = form.elements.password;
|
|
175
177
|
const repeat = form.elements.passwordRepeat;
|
|
176
178
|
const show = form.elements.show;
|
|
@@ -289,9 +291,15 @@
|
|
|
289
291
|
|
|
290
292
|
const installIntegratedFirstLogin = base => {
|
|
291
293
|
const t = translations[language()];
|
|
292
|
-
const managedAccounts = new Set(['installer', 'guest']);
|
|
294
|
+
const managedAccounts = new Set(['installer', 'guest', 'user']);
|
|
293
295
|
let busy = false;
|
|
294
296
|
let unsubscribeDom = null;
|
|
297
|
+
let statusTimer = 0;
|
|
298
|
+
let statusSequence = 0;
|
|
299
|
+
let submitObserver = null;
|
|
300
|
+
let observedSubmit = null;
|
|
301
|
+
let loginStateMonitor = 0;
|
|
302
|
+
const eligibility = { account: '', allowed: false, checking: false, loaded: false };
|
|
295
303
|
|
|
296
304
|
document.documentElement.classList.add('eos-modern-login', 'eos-integrated-first-login');
|
|
297
305
|
document.querySelectorAll('.eos-passwordless-launcher').forEach(element => element.remove());
|
|
@@ -312,35 +320,156 @@
|
|
|
312
320
|
return { username, password, card, submit };
|
|
313
321
|
};
|
|
314
322
|
const statusNode = card => card?.querySelector?.('.eos-login-first-status');
|
|
323
|
+
const directChildWithin = (ancestor, node) => {
|
|
324
|
+
let current = node;
|
|
325
|
+
while (current && current.parentElement && current.parentElement !== ancestor) current = current.parentElement;
|
|
326
|
+
return current?.parentElement === ancestor ? current : null;
|
|
327
|
+
};
|
|
315
328
|
const setStatus = (card, message, kind = '') => {
|
|
316
329
|
const status = statusNode(card);
|
|
317
330
|
if (!status) return;
|
|
318
331
|
status.textContent = message || '';
|
|
319
332
|
status.dataset.kind = kind;
|
|
320
333
|
};
|
|
334
|
+
const setSubmitDisabled = (submit, disabled) => {
|
|
335
|
+
if (!submit) return;
|
|
336
|
+
const nextDisabled = !!disabled;
|
|
337
|
+
// Idempotent writes are essential here: the submit MutationObserver invokes
|
|
338
|
+
// updateLoginState again. Rewriting unchanged attributes would create a feedback loop.
|
|
339
|
+
if (submit.disabled !== nextDisabled) submit.disabled = nextDisabled;
|
|
340
|
+
if (submit.classList.contains('Mui-disabled') !== nextDisabled) submit.classList.toggle('Mui-disabled', nextDisabled);
|
|
341
|
+
const aria = nextDisabled ? 'true' : 'false';
|
|
342
|
+
if (submit.getAttribute('aria-disabled') !== aria) submit.setAttribute('aria-disabled', aria);
|
|
343
|
+
const tabIndex = nextDisabled ? -1 : 0;
|
|
344
|
+
if (submit.tabIndex !== tabIndex) submit.tabIndex = tabIndex;
|
|
345
|
+
if (nextDisabled) {
|
|
346
|
+
if (!submit.hasAttribute('disabled')) submit.setAttribute('disabled', '');
|
|
347
|
+
if (submit.hasAttribute('data-eos-passwordless-submit')) submit.removeAttribute('data-eos-passwordless-submit');
|
|
348
|
+
} else {
|
|
349
|
+
if (submit.hasAttribute('disabled')) submit.removeAttribute('disabled');
|
|
350
|
+
if (submit.getAttribute('data-eos-passwordless-submit') !== '1') submit.dataset.eosPasswordlessSubmit = '1';
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const syncPasswordRequirement = (password, optional) => {
|
|
354
|
+
if (!password) return;
|
|
355
|
+
password.required = !optional;
|
|
356
|
+
password.setAttribute('aria-required', optional ? 'false' : 'true');
|
|
357
|
+
password.toggleAttribute('data-eos-first-login-optional', !!optional);
|
|
358
|
+
password.closest?.('.MuiFormControl-root')?.classList?.toggle('eos-first-login-password-optional', !!optional);
|
|
359
|
+
};
|
|
360
|
+
const eligibilityMatches = account => eligibility.loaded && eligibility.account === account;
|
|
361
|
+
const isEligibleBlankAccount = (account, password) => managedAccounts.has(account)
|
|
362
|
+
&& !password?.value
|
|
363
|
+
&& eligibilityMatches(account)
|
|
364
|
+
&& eligibility.allowed === true;
|
|
321
365
|
const updateLoginState = () => {
|
|
322
366
|
const { username, password, card, submit } = loginElements();
|
|
323
|
-
if (!username || !password || !card) return;
|
|
367
|
+
if (!username || !password || !card || !submit) return;
|
|
324
368
|
const account = accountName(username);
|
|
325
369
|
const blankManaged = managedAccounts.has(account) && !password.value;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
370
|
+
const ready = isEligibleBlankAccount(account, password) && !busy;
|
|
371
|
+
const checking = blankManaged && eligibility.account === account && eligibility.checking;
|
|
372
|
+
card.classList.toggle('eos-login-first-ready', ready);
|
|
373
|
+
card.classList.toggle('eos-login-first-checking', checking);
|
|
374
|
+
card.classList.toggle('eos-login-first-blocked', blankManaged && eligibilityMatches(account) && !eligibility.allowed);
|
|
375
|
+
syncPasswordRequirement(password, ready);
|
|
376
|
+
if (blankManaged) {
|
|
377
|
+
setSubmitDisabled(submit, !ready);
|
|
378
|
+
} else {
|
|
379
|
+
setSubmitDisabled(submit, busy || !account || !password.value);
|
|
380
|
+
if (!password.value) submit.removeAttribute('data-eos-passwordless-submit');
|
|
334
381
|
}
|
|
335
382
|
const hint = card.querySelector('.eos-login-first-hint');
|
|
336
|
-
if (hint)
|
|
383
|
+
if (hint) {
|
|
384
|
+
if (checking) hint.textContent = t.claimChecking;
|
|
385
|
+
else if (ready) hint.textContent = t.claimIntro;
|
|
386
|
+
else if (blankManaged && eligibilityMatches(account)) hint.textContent = t.claimAlreadyActive;
|
|
387
|
+
else hint.textContent = t.claimHint;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const ensureSubmitObserver = submit => {
|
|
391
|
+
if (!submit || observedSubmit === submit) return;
|
|
392
|
+
submitObserver?.disconnect();
|
|
393
|
+
observedSubmit = submit;
|
|
394
|
+
submitObserver = new MutationObserver(() => queueMicrotask(updateLoginState));
|
|
395
|
+
submitObserver.observe(submit, {
|
|
396
|
+
attributes: true,
|
|
397
|
+
attributeFilter: ['disabled', 'class', 'aria-disabled', 'tabindex'],
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
const requestEligibility = async account => {
|
|
401
|
+
const normalized = String(account || '').trim().toLowerCase().replace(/^system\.user\./, '');
|
|
402
|
+
const sequence = ++statusSequence;
|
|
403
|
+
eligibility.account = normalized;
|
|
404
|
+
eligibility.allowed = false;
|
|
405
|
+
eligibility.loaded = false;
|
|
406
|
+
eligibility.checking = managedAccounts.has(normalized);
|
|
407
|
+
updateLoginState();
|
|
408
|
+
if (!managedAccounts.has(normalized)) {
|
|
409
|
+
eligibility.checking = false;
|
|
410
|
+
eligibility.loaded = true;
|
|
411
|
+
updateLoginState();
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const response = await fetch(new URL('nexowatt/account/passwordless-status', base).href, {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
credentials: 'same-origin',
|
|
418
|
+
cache: 'no-store',
|
|
419
|
+
headers: {
|
|
420
|
+
'Content-Type': 'application/json',
|
|
421
|
+
Accept: 'application/json',
|
|
422
|
+
'X-NexoWatt-EOS-Passwordless-Status': '1',
|
|
423
|
+
},
|
|
424
|
+
body: JSON.stringify({ user: normalized }),
|
|
425
|
+
});
|
|
426
|
+
const data = await response.json().catch(() => ({}));
|
|
427
|
+
if (sequence !== statusSequence) return false;
|
|
428
|
+
eligibility.allowed = response.ok && data?.eligible === true;
|
|
429
|
+
eligibility.loaded = true;
|
|
430
|
+
eligibility.checking = false;
|
|
431
|
+
const { card } = loginElements();
|
|
432
|
+
setStatus(card, response.ok ? '' : t.claimStatusError, response.ok ? '' : 'error');
|
|
433
|
+
updateLoginState();
|
|
434
|
+
return eligibility.allowed;
|
|
435
|
+
} catch (_) {
|
|
436
|
+
if (sequence !== statusSequence) return false;
|
|
437
|
+
eligibility.allowed = false;
|
|
438
|
+
eligibility.loaded = true;
|
|
439
|
+
eligibility.checking = false;
|
|
440
|
+
const { card } = loginElements();
|
|
441
|
+
setStatus(card, t.claimStatusError, 'error');
|
|
442
|
+
updateLoginState();
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const scheduleEligibilityCheck = () => {
|
|
447
|
+
window.clearTimeout(statusTimer);
|
|
448
|
+
statusTimer = window.setTimeout(() => {
|
|
449
|
+
const { username, password } = loginElements();
|
|
450
|
+
const account = accountName(username);
|
|
451
|
+
if (password?.value || !managedAccounts.has(account)) {
|
|
452
|
+
statusSequence += 1;
|
|
453
|
+
eligibility.account = account;
|
|
454
|
+
eligibility.allowed = false;
|
|
455
|
+
eligibility.checking = false;
|
|
456
|
+
eligibility.loaded = true;
|
|
457
|
+
updateLoginState();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
void requestEligibility(account);
|
|
461
|
+
}, 120);
|
|
337
462
|
};
|
|
338
463
|
const startClaim = async requestedAccount => {
|
|
339
464
|
const { username, password, card, submit } = loginElements();
|
|
340
|
-
const account = String(requestedAccount || accountName(username)).trim().toLowerCase();
|
|
465
|
+
const account = String(requestedAccount || accountName(username)).trim().toLowerCase().replace(/^system\.user\./, '');
|
|
341
466
|
if (!managedAccounts.has(account) || password?.value || busy) return false;
|
|
467
|
+
if (!eligibilityMatches(account) || !eligibility.allowed) {
|
|
468
|
+
const allowed = await requestEligibility(account);
|
|
469
|
+
if (!allowed) return false;
|
|
470
|
+
}
|
|
342
471
|
busy = true;
|
|
343
|
-
if (submit) submit
|
|
472
|
+
if (submit) setSubmitDisabled(submit, true);
|
|
344
473
|
setStatus(card, t.claimChecking, '');
|
|
345
474
|
try {
|
|
346
475
|
const response = await fetch(new URL('nexowatt/account/passwordless-claim', base).href, {
|
|
@@ -357,6 +486,8 @@
|
|
|
357
486
|
showPasswordlessClaimPassword(data, base);
|
|
358
487
|
return true;
|
|
359
488
|
} catch (error) {
|
|
489
|
+
eligibility.allowed = false;
|
|
490
|
+
eligibility.loaded = true;
|
|
360
491
|
setStatus(card, error?.message || t.claimUnavailable, 'error');
|
|
361
492
|
return false;
|
|
362
493
|
} finally {
|
|
@@ -366,38 +497,36 @@
|
|
|
366
497
|
};
|
|
367
498
|
const ensureIntegratedLogin = () => {
|
|
368
499
|
document.querySelectorAll('.eos-passwordless-launcher').forEach(element => element.remove());
|
|
369
|
-
const { username, password, card } = loginElements();
|
|
500
|
+
const { username, password, card, submit } = loginElements();
|
|
370
501
|
if (!username || !password || !card) return;
|
|
371
502
|
card.classList.add('eos-login-card-modern');
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
selector.innerHTML = `
|
|
378
|
-
<div class="eos-login-role-heading"><strong>${t.claimAccount}</strong><span>${t.claimHint}</span></div>
|
|
379
|
-
<div class="eos-login-role-buttons">
|
|
380
|
-
<button type="button" data-eos-account="admin"><span>◆</span><strong>Admin / Service</strong></button>
|
|
381
|
-
<button type="button" data-eos-account="installer"><span>⚙</span><strong>Installateur</strong></button>
|
|
382
|
-
<button type="button" data-eos-account="guest"><span>⌂</span><strong>Gast / Endkunde</strong></button>
|
|
383
|
-
</div>
|
|
384
|
-
<p class="eos-login-first-hint"></p>
|
|
385
|
-
<div class="eos-login-first-status" aria-live="polite"></div>`;
|
|
386
|
-
const usernameControl = username.closest('.MuiFormControl-root') || username.parentElement;
|
|
387
|
-
card.insertBefore(selector, usernameControl || card.firstChild);
|
|
388
|
-
selector.querySelectorAll('[data-eos-account]').forEach(button => button.addEventListener('click', () => {
|
|
389
|
-
setInputValue(username, button.dataset.eosAccount || '');
|
|
390
|
-
setInputValue(password, '');
|
|
391
|
-
window.setTimeout(() => password.focus(), 0);
|
|
392
|
-
updateLoginState();
|
|
393
|
-
}));
|
|
394
|
-
}
|
|
503
|
+
// v91 compact normal-login first activation: do not add role buttons or a second panel.
|
|
504
|
+
// The account is entered in the existing Loginname field; a blank password is accepted only
|
|
505
|
+
// after the backend confirms that this exact managed account is eligible.
|
|
506
|
+
card.querySelectorAll('.eos-login-role-selector').forEach(element => element.remove());
|
|
507
|
+
card.dataset.eosCompactFirstLogin = '1';
|
|
395
508
|
if (!username.dataset.eosFirstLoginBound) {
|
|
396
509
|
username.dataset.eosFirstLoginBound = '1';
|
|
397
|
-
username.addEventListener('input',
|
|
398
|
-
|
|
510
|
+
username.addEventListener('input', scheduleEligibilityCheck);
|
|
511
|
+
username.addEventListener('change', scheduleEligibilityCheck);
|
|
512
|
+
password.addEventListener('input', scheduleEligibilityCheck);
|
|
513
|
+
password.addEventListener('change', scheduleEligibilityCheck);
|
|
399
514
|
}
|
|
515
|
+
ensureSubmitObserver(submit);
|
|
400
516
|
updateLoginState();
|
|
517
|
+
scheduleEligibilityCheck();
|
|
518
|
+
};
|
|
519
|
+
const startLoginStateMonitor = () => {
|
|
520
|
+
if (loginStateMonitor) return;
|
|
521
|
+
loginStateMonitor = window.setInterval(() => {
|
|
522
|
+
const { username, password, submit } = loginElements();
|
|
523
|
+
if (!username || !password || !submit) {
|
|
524
|
+
ensureIntegratedLogin();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
ensureSubmitObserver(submit);
|
|
528
|
+
updateLoginState();
|
|
529
|
+
}, 350);
|
|
401
530
|
};
|
|
402
531
|
|
|
403
532
|
document.addEventListener('click', event => {
|
|
@@ -407,7 +536,8 @@
|
|
|
407
536
|
if (!managedAccounts.has(account) || password?.value) return;
|
|
408
537
|
event.preventDefault();
|
|
409
538
|
event.stopImmediatePropagation();
|
|
410
|
-
void startClaim(account);
|
|
539
|
+
if (isEligibleBlankAccount(account, password)) void startClaim(account);
|
|
540
|
+
else void requestEligibility(account);
|
|
411
541
|
}, { capture: true });
|
|
412
542
|
document.addEventListener('keydown', event => {
|
|
413
543
|
if (event.key !== 'Enter') return;
|
|
@@ -417,7 +547,7 @@
|
|
|
417
547
|
if (!managedAccounts.has(account) || password?.value) return;
|
|
418
548
|
event.preventDefault();
|
|
419
549
|
event.stopImmediatePropagation();
|
|
420
|
-
void startClaim(account);
|
|
550
|
+
if (isEligibleBlankAccount(account, password)) void startClaim(account);
|
|
421
551
|
}, { capture: true });
|
|
422
552
|
|
|
423
553
|
const connectCoordinator = () => {
|
|
@@ -429,12 +559,16 @@
|
|
|
429
559
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ensureIntegratedLogin, { once: true });
|
|
430
560
|
else ensureIntegratedLogin();
|
|
431
561
|
connectCoordinator();
|
|
432
|
-
|
|
562
|
+
startLoginStateMonitor();
|
|
563
|
+
window.addEventListener('focus', updateLoginState);
|
|
564
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) updateLoginState(); });
|
|
565
|
+
[100, 250, 500, 900, 1500, 2600, 5000].forEach(delay => window.setTimeout(ensureIntegratedLogin, delay));
|
|
433
566
|
|
|
434
567
|
window.NEXOWATT_EOS_FIRST_LOGIN = Object.freeze({
|
|
435
568
|
version: VERSION,
|
|
436
569
|
start: account => startClaim(account),
|
|
437
570
|
refresh: ensureIntegratedLogin,
|
|
571
|
+
check: account => requestEligibility(account),
|
|
438
572
|
});
|
|
439
573
|
};
|
|
440
574
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const VERSION = '
|
|
4
|
+
const VERSION = 'v91-final-clickable-overview-live-reserve-filter';
|
|
5
5
|
window.NEXOWATT_EOS_ROLE_UI_VERSION = VERSION;
|
|
6
6
|
|
|
7
7
|
const state = {
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
lastTarget: '',
|
|
13
13
|
unsubscribePolicy: null,
|
|
14
14
|
unsubscribeDom: null,
|
|
15
|
+
reserveObserver: null,
|
|
16
|
+
reservePaper: null,
|
|
17
|
+
reserveScheduled: false,
|
|
15
18
|
};
|
|
16
19
|
const abort = new AbortController();
|
|
17
20
|
|
|
@@ -94,13 +97,23 @@
|
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
const textOf = el => [el?.textContent || '', ...['href','title','aria-label','data-name','data-tab','data-id','data-instance','id'].map(a => el?.getAttribute?.(a) || '')].join(' ');
|
|
97
|
-
const navEntries = () =>
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
return
|
|
103
|
-
|
|
100
|
+
const navEntries = () => {
|
|
101
|
+
const candidates = Array.from(document.querySelectorAll(
|
|
102
|
+
'[data-eos-tab],a[href*="#tab-"],a[href*="#/tab-"],a[href="#easy"],a[href="/#easy"],a[href$="/#easy"]',
|
|
103
|
+
));
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
return candidates.map(control => {
|
|
106
|
+
const href = control.getAttribute?.('href') || control.href || '';
|
|
107
|
+
const declared = normalize(control.getAttribute?.('data-eos-tab') || '');
|
|
108
|
+
const tab = declared || (/^#\/?easy(?:[/?&]|$)/i.test(href) ? 'easy' : tabFromHref(href));
|
|
109
|
+
if (!tab) return null;
|
|
110
|
+
const root = control.closest?.('.MuiListItem-root,.MuiButtonBase-root,li,[role="button"],[class*="DrawerItem"],[class*="dragWrapper"]') || control;
|
|
111
|
+
const key = `${tab}|${textOf(root)}`;
|
|
112
|
+
if (seen.has(key)) return null;
|
|
113
|
+
seen.add(key);
|
|
114
|
+
return { anchor: control, root, tab, label: textOf(root) || textOf(control) };
|
|
115
|
+
}).filter(Boolean);
|
|
116
|
+
};
|
|
104
117
|
const firstAllowedTab = role => navEntries().find(entry => entry.tab === 'tab-intro' && isRouteAllowed(role, entry.tab, entry.label))?.tab
|
|
105
118
|
|| navEntries().find(entry => isRouteAllowed(role, entry.tab, entry.label))?.tab || defaultTab(role);
|
|
106
119
|
|
|
@@ -172,6 +185,28 @@
|
|
|
172
185
|
if (window.location.hash !== targetHash) window.location.hash = targetHash;
|
|
173
186
|
window.dispatchEvent(new CustomEvent('eos-role-navigate', { detail: { tab } }));
|
|
174
187
|
};
|
|
188
|
+
const navigateToTab = (tab, label = '') => {
|
|
189
|
+
if (!tab) return false;
|
|
190
|
+
if (tab === 'basic-settings') {
|
|
191
|
+
window.NEXOWATT_EOS_BASIC_SETTINGS?.open?.();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (!isRouteAllowed(state.role, tab, label)) return false;
|
|
195
|
+
const entry = navEntries().find(candidate =>
|
|
196
|
+
candidate.tab === tab
|
|
197
|
+
&& isRouteAllowed(state.role, candidate.tab, candidate.label)
|
|
198
|
+
&& !candidate.anchor.closest?.('#eos-role-safe-overview'),
|
|
199
|
+
);
|
|
200
|
+
if (entry?.anchor) {
|
|
201
|
+
entry.anchor.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
202
|
+
window.setTimeout(() => {
|
|
203
|
+
if (currentRoute() !== tab) setHashTab(tab);
|
|
204
|
+
}, 80);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
setHashTab(tab);
|
|
208
|
+
return true;
|
|
209
|
+
};
|
|
175
210
|
const redirectIfNeeded = () => safe(() => {
|
|
176
211
|
if (isLoginView() || state.role === 'unknown') return;
|
|
177
212
|
const route = currentRoute();
|
|
@@ -240,29 +275,117 @@
|
|
|
240
275
|
<div class="eos-overview-role"><span class="eos-overview-status-dot"></span>${installer ? 'Installateur' : 'Gast / Endkunde'}</div>
|
|
241
276
|
</header>
|
|
242
277
|
<div class="eos-overview-grid">${actions.map(([label, description, tab, icon]) => `
|
|
243
|
-
<
|
|
278
|
+
<a class="eos-overview-action" data-eos-role-tab="${tab}" href="${tab === 'basic-settings' ? '#tab-intro' : `#${tab}`}">
|
|
244
279
|
<span class="eos-overview-action-icon" aria-hidden="true">${iconFor(icon)}</span>
|
|
245
280
|
<span><strong>${label}</strong><small>${description}</small></span><i aria-hidden="true">›</i>
|
|
246
|
-
</
|
|
281
|
+
</a>`).join('')}</div>`;
|
|
282
|
+
overview.querySelectorAll('a[data-eos-role-tab]').forEach(link => {
|
|
283
|
+
link.addEventListener('click', event => {
|
|
284
|
+
event.preventDefault();
|
|
285
|
+
event.stopPropagation();
|
|
286
|
+
const targetTab = link.getAttribute('data-eos-role-tab') || '';
|
|
287
|
+
if (targetTab) navigateToTab(targetTab, link.textContent || '');
|
|
288
|
+
});
|
|
289
|
+
link.addEventListener('keydown', event => {
|
|
290
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
navigateToTab(link.getAttribute('data-eos-role-tab') || '', link.textContent || '');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
247
295
|
});
|
|
248
296
|
|
|
249
297
|
const hideOfficialReserveSurfaces = () => safe(() => {
|
|
250
|
-
if (state.role === 'admin') return;
|
|
251
298
|
const paper = appPaper();
|
|
252
299
|
if (!paper) return;
|
|
253
|
-
const
|
|
300
|
+
const rowSelector = [
|
|
301
|
+
'tr', '.MuiTableRow-root', '.MuiAccordion-root', '.MuiCard-root', '.MuiListItem-root',
|
|
302
|
+
'[role="row"]', '[data-instance-row]', '[data-testid="adapter-card"]', '[data-testid="instance-card"]',
|
|
303
|
+
].join(',');
|
|
304
|
+
const reserveId = /^(?:system\.adapter\.)?(?:admin|backitup)\.\d+$/i;
|
|
305
|
+
const normalizeInstance = value => String(value || '').trim().replace(/^system\.adapter\./i, '');
|
|
306
|
+
const identityValues = node => {
|
|
307
|
+
if (!node) return [];
|
|
308
|
+
const attrs = ['data-id', 'data-instance', 'data-name', 'data-adapter', 'data-instance-id', 'id'];
|
|
309
|
+
const values = attrs.map(name => node.getAttribute?.(name) || '');
|
|
310
|
+
node.querySelectorAll?.('[data-id],[data-instance],[data-name],[data-adapter],[data-instance-id]').forEach(child => {
|
|
311
|
+
attrs.forEach(name => values.push(child.getAttribute?.(name) || ''));
|
|
312
|
+
});
|
|
313
|
+
return values.map(normalizeInstance).filter(Boolean);
|
|
314
|
+
};
|
|
315
|
+
const rowTextContainsReserve = row => {
|
|
316
|
+
const text = String(row?.textContent || '')
|
|
317
|
+
.replace(/eos-admin\.\d+/gi, '')
|
|
318
|
+
.replace(/nexowatt-backup\.\d+/gi, '')
|
|
319
|
+
.replace(/eos-backup\.\d+/gi, '')
|
|
320
|
+
.replace(/system\.adapter\.(?:eos-admin|nexowatt-backup|eos-backup)\.\d+/gi, '');
|
|
321
|
+
return /(?:^|[\s|,;])(?:system\.adapter\.)?(?:admin|backitup)\.\d+(?=$|[\s|,;])/i.test(text.trim());
|
|
322
|
+
};
|
|
323
|
+
const rowIsReserve = row => identityValues(row).some(value => reserveId.test(value)) || rowTextContainsReserve(row);
|
|
324
|
+
const setHidden = (row, hidden) => {
|
|
325
|
+
if (!row || row === paper || row.id === 'app-paper' || row.classList?.contains('eos-role-safe-overview')) return;
|
|
326
|
+
row.classList.toggle('eos-role-hidden', hidden);
|
|
327
|
+
row.classList.toggle('eos-official-reserve-hidden', hidden);
|
|
328
|
+
if (hidden) {
|
|
329
|
+
row.setAttribute('aria-hidden', 'true');
|
|
330
|
+
row.setAttribute('data-eos-official-reserve-hidden', '1');
|
|
331
|
+
} else {
|
|
332
|
+
row.removeAttribute('aria-hidden');
|
|
333
|
+
row.removeAttribute('data-eos-official-reserve-hidden');
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const rowFor = node => node?.matches?.(rowSelector) ? node : node?.closest?.(rowSelector);
|
|
337
|
+
|
|
338
|
+
// Reused virtual rows must be unhidden when their identity changes.
|
|
339
|
+
paper.querySelectorAll('[data-eos-official-reserve-hidden="1"]').forEach(node => {
|
|
340
|
+
const row = rowFor(node) || node;
|
|
341
|
+
if (state.role === 'admin' || !rowIsReserve(row)) setHidden(row, false);
|
|
342
|
+
});
|
|
343
|
+
if (state.role === 'admin') return;
|
|
344
|
+
|
|
345
|
+
paper.querySelectorAll(rowSelector).forEach(row => setHidden(row, rowIsReserve(row)));
|
|
346
|
+
paper.querySelectorAll([
|
|
254
347
|
'[data-id^="system.adapter.admin"]','[data-id^="system.adapter.backitup"]',
|
|
255
348
|
'[data-instance^="admin."]','[data-instance^="backitup."]',
|
|
349
|
+
'[data-instance-id^="admin."]','[data-instance-id^="backitup."]',
|
|
256
350
|
'[data-name="admin"]','[data-name="backitup"]',
|
|
257
351
|
'a[href*="#tab-admin"]','a[href*="#tab-backitup"]',
|
|
258
352
|
'a[href*="system.adapter.admin"]','a[href*="system.adapter.backitup"]',
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
353
|
+
].join(',')).forEach(node => setHidden(rowFor(node) || node, true));
|
|
354
|
+
|
|
355
|
+
// Admin 7 table cells may have no metadata. Match only an exact visible instance ID and
|
|
356
|
+
// hide the complete Accordion/Card/Table row, never the NexoWatt replacements.
|
|
357
|
+
const walker = document.createTreeWalker(paper, NodeFilter.SHOW_TEXT);
|
|
358
|
+
let textNode;
|
|
359
|
+
while ((textNode = walker.nextNode())) {
|
|
360
|
+
const value = String(textNode.textContent || '').trim();
|
|
361
|
+
if (reserveId.test(value)) setHidden(rowFor(textNode.parentElement), true);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const ensureReserveObserver = () => safe(() => {
|
|
366
|
+
const paper = appPaper();
|
|
367
|
+
if (!paper || state.role === 'admin') {
|
|
368
|
+
state.reserveObserver?.disconnect?.();
|
|
369
|
+
state.reserveObserver = null;
|
|
370
|
+
state.reservePaper = null;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (state.reserveObserver && state.reservePaper === paper) return;
|
|
374
|
+
state.reserveObserver?.disconnect?.();
|
|
375
|
+
state.reservePaper = paper;
|
|
376
|
+
state.reserveObserver = new MutationObserver(mutations => {
|
|
377
|
+
if (!mutations.some(mutation => mutation.addedNodes?.length)) return;
|
|
378
|
+
if (state.reserveScheduled) return;
|
|
379
|
+
state.reserveScheduled = true;
|
|
380
|
+
const run = () => {
|
|
381
|
+
state.reserveScheduled = false;
|
|
382
|
+
hideOfficialReserveSurfaces();
|
|
383
|
+
};
|
|
384
|
+
if ('requestAnimationFrame' in window) requestAnimationFrame(run); else setTimeout(run, 16);
|
|
265
385
|
});
|
|
386
|
+
// This observer is deliberately scoped to #app-paper. It catches rows created or recycled
|
|
387
|
+
// after the shared coordinator has already processed the high-load services view.
|
|
388
|
+
state.reserveObserver.observe(paper, { childList: true, subtree: true });
|
|
266
389
|
});
|
|
267
390
|
|
|
268
391
|
const applySensitiveDialogPolicy = () => safe(() => {
|
|
@@ -297,6 +420,7 @@
|
|
|
297
420
|
redirectIfNeeded();
|
|
298
421
|
ensureSafeOverview();
|
|
299
422
|
hideOfficialReserveSurfaces();
|
|
423
|
+
ensureReserveObserver();
|
|
300
424
|
suppressPermissionErrors();
|
|
301
425
|
window.NEXOWATT_EOS_ACCOUNT_MANAGEMENT?.refreshEntry?.();
|
|
302
426
|
};
|
|
@@ -325,9 +449,15 @@
|
|
|
325
449
|
if (!coordinator?.subscribe) { setTimeout(startObserver, 200); return; }
|
|
326
450
|
if (state.unsubscribeDom) return;
|
|
327
451
|
state.unsubscribeDom = coordinator.subscribe(mutations => {
|
|
328
|
-
if (isHighLoadSurface() && mutations.length && mutations.every(m => isInsideAppPaper(m.target) || isInsideAppPaper(m.addedNodes?.[0])))
|
|
452
|
+
if (isHighLoadSurface() && mutations.length && mutations.every(m => isInsideAppPaper(m.target) || isInsideAppPaper(m.addedNodes?.[0]))) {
|
|
453
|
+
// Keep virtualized service/adapter tables light, but still enforce the exact internal
|
|
454
|
+
// reserve visibility rule whenever rows arrive after the initial render.
|
|
455
|
+
hideOfficialReserveSurfaces();
|
|
456
|
+
suppressPermissionErrors();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
329
459
|
scheduleApply();
|
|
330
|
-
});
|
|
460
|
+
}, { includeTableMutations: true });
|
|
331
461
|
};
|
|
332
462
|
|
|
333
463
|
document.addEventListener('click', event => {
|
|
@@ -341,7 +471,7 @@
|
|
|
341
471
|
if (state.role === 'installer') window.NEXOWATT_EOS_BASIC_SETTINGS?.open?.();
|
|
342
472
|
return;
|
|
343
473
|
}
|
|
344
|
-
const target = event.target?.closest?.('[data-eos-admin-only-control="1"],[data-eos-system-settings-control="1"],
|
|
474
|
+
const target = event.target?.closest?.('[data-eos-admin-only-control="1"],[data-eos-system-settings-control="1"],[data-eos-role-tab],a[href*="#tab-"],a[href*="#/tab-"]');
|
|
345
475
|
if (!target) return;
|
|
346
476
|
if (target.matches('[data-eos-admin-only-control="1"]')) { event.preventDefault(); event.stopImmediatePropagation(); return; }
|
|
347
477
|
if (target.matches('[data-eos-system-settings-control="1"]') && state.role === 'installer') {
|
|
@@ -350,8 +480,8 @@
|
|
|
350
480
|
const roleTab = target.getAttribute('data-eos-role-tab');
|
|
351
481
|
if (roleTab) {
|
|
352
482
|
event.preventDefault();
|
|
353
|
-
|
|
354
|
-
|
|
483
|
+
event.stopImmediatePropagation();
|
|
484
|
+
navigateToTab(roleTab, target.textContent || '');
|
|
355
485
|
return;
|
|
356
486
|
}
|
|
357
487
|
const tab = tabFromHref(target.getAttribute('href') || target.href || '');
|
|
@@ -368,6 +498,9 @@
|
|
|
368
498
|
defaultTab,
|
|
369
499
|
isOfficialReserveTab,
|
|
370
500
|
isCustomerBackupTab,
|
|
501
|
+
navigateToTab,
|
|
502
|
+
ensureSafeOverview,
|
|
503
|
+
hideOfficialReserveSurfaces,
|
|
371
504
|
refresh: scheduleApply,
|
|
372
505
|
});
|
|
373
506
|
const start = () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const VERSION = '
|
|
4
|
+
const VERSION = 'v91-nexowatt-native-shell-modern-overview';
|
|
5
5
|
const previous = window.NEXOWATT_NATIVE_SHELL;
|
|
6
6
|
if (previous?.version === VERSION) return;
|
|
7
7
|
previous?.destroy?.();
|